diff --git a/README.md b/README.md index 52fcd2e..bda0dea 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,23 @@ echo "ELEVENLABS_API_KEY=..." > .env # read from the environment or ./.env ## Command line ```sh -npx avatarscript make --avatar avatars/ani \ - --script examples/hello-ja.avs --tts elevenlabs -o out/hello-ja.mp4 +npx avatarscript make --avatar https://raw.githubusercontent.com/receptron/mulmocast-media/main/avatars/ani \ + --script example:hello-ja --tts elevenlabs -o out/hello-ja.mp4 ``` +`--script example:` reads a sample script shipped with the package +([`examples/*.avs`](examples): `hello-ja`, `hello-en`, `subtitles-ja`); `--script` otherwise takes +your own `.avs` or plain `.txt` file. + +> **`npx avatarscript` does not work inside a clone of this repository.** npx finds the project +> itself instead of the published package, and a package is never linked into its own +> `node_modules/.bin`, so it fails with `sh: avatarscript: command not found`, even after +> `npm run build`. Run it from any other folder, or in the clone use one of: +> +> - `npm run avatarscript -- make ...`: the CLI from source +> - `node dist/cli.js make ...`: the built CLI, after `npm run build` +> - `npx avatarscript@latest make ...`: the published package, fetched from npm + This writes `out/hello-ja.wav`, `out/hello-ja.score.json` and `out/hello-ja.mp4`. Synthesized speech is cached per segment in `out/.tts-cache/`, so changing motions or one sentence does not request the whole script again. `--tts mock` runs the pipeline offline with a buzzing stand-in @@ -44,14 +57,9 @@ voice. The steps can also be run separately with `compile` and `render`; run `--avatar` takes a mesh-avatar-studio project folder (`rig.json` + `built/`) or a folder with an `avatar.json` manifest, on disk or at an http(s) URL. The repository includes one sample avatar, -[`avatars/ani`](avatars/ani) (not in the npm package). It is also published in +[`avatars/ani`](avatars/ani) (not in the npm package); the URL above is the same avatar, published in [mulmocast-media](https://github.com/receptron/mulmocast-media/tree/main/avatars), so it works -without a clone: - -```sh -npx avatarscript make --avatar https://raw.githubusercontent.com/receptron/mulmocast-media/main/avatars/ani \ - --script hello.avs --tts elevenlabs -o out/hello.mp4 -``` +without a clone. From a URL, the package's small JSON files are fetched each time and its images are cached in `~/.cache/avatarscript/avatars/` (or `$AVATARSCRIPT_AVATAR_DIR`), per build of the avatar, so an diff --git a/package.json b/package.json index e2d7f3c..8d2e344 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ "files": [ "dist", "README.md", - "LICENSE" + "LICENSE", + "examples/*.avs" ], "engines": { "node": ">=22.18" diff --git a/plans/feat-bundled-example-scripts.md b/plans/feat-bundled-example-scripts.md new file mode 100644 index 0000000..02889c7 --- /dev/null +++ b/plans/feat-bundled-example-scripts.md @@ -0,0 +1,33 @@ +# Bundled example scripts (`--script example:`) + +Issue: #7 + +## Problem + +`npx avatarscript make --avatar avatars/ani --script examples/hello-ja.avs ...` (the README's first +example) fails inside a clone (npx resolves to the project itself, which has no `.bin` entry) and +outside one (the relative paths are not in the npm package). Nothing lets a user try the CLI with +npx alone. + +## Change + +- `package.json` `files`: add `examples/*.avs`, so the sample scripts ship with the package. +- `src/examples.ts` (pure, no fs): `exampleNames(files)` lists bundled names from a directory + listing; `scriptSource(arg, examplesDir, names)` maps a `--script` value to the file to read — + `example:` → `/.avs`, anything else unchanged; an unknown name throws + with the list. +- `src/cli.ts`: read the `examples/` folder next to `dist/` (or `src/`), route `--script` through + `scriptSource`, list the examples in `--help`, and record `example:` (not a path into the + npx cache) as the score's script provenance. +- README: the first command-line example becomes npx-only (avatar URL + `example:hello-ja`), followed + by a prominent note that `npx avatarscript` fails inside a clone, even after `npm run build`, and + the three commands that work there instead. + +## Not in scope + +A default `--avatar`; fetching scripts from URLs (see the issue for why). + +## Verification + +Unit tests for `src/examples.ts`; `scripts/smoke.sh avatars/ani` extended to compile +`--script example:hello-en` from the installed tarball, so the packaging is checked, not assumed. diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 62176b8..cc0a103 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Package smoke test: pack the exact tarball (prepack builds dist/ and bundles the engine), # install it into an empty folder with no mesh-avatar package and no esbuild beside it, -# and make a video through both the CLI and the library with the offline mock voice. +# and make a video through both the CLI and the library with the offline mock voice, and compile +# a bundled example script (--script example:). # Then check the video is real: an H.264 + AAC MP4 of the expected length whose frames show # the avatar, not a blank canvas. # @@ -27,6 +28,11 @@ fi printf '[emotion:happy] こんにちは! Hello from the package.\n' > story.avs node_modules/.bin/avatarscript make --avatar "$AVATAR" --script story.avs --tts mock \ --size 640x360 -o out/cli.mp4 +# compile only: enough to show the bundled script shipped and resolves from the installed package +node_modules/.bin/avatarscript compile --avatar "$AVATAR" --script example:hello-en --tts mock \ + -o out/example.score.json +grep -q '"script": "example:hello-en"' out/example.score.json \ + || { echo "bundled example: provenance missing from out/example.score.json"; exit 1; } cat > lib.mjs < --script -o [options] - avatarscript compile --avatar --script -o [options] + avatarscript make --avatar --script -o [options] + avatarscript compile --avatar --script -o [options] avatarscript render --avatar --score -o [options] Speech (make, compile): @@ -42,6 +43,16 @@ Video (make, render): API keys are read from the environment or a .env file.`; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const examplesDir = join(repoRoot, "examples"); + +function bundledExamples(): string[] { + try { + return exampleNames(readdirSync(examplesDir)); + } catch { + return []; + } +} + for (const file of [resolve(".env"), join(repoRoot, ".env")]) { if (existsSync(file)) { process.loadEnvFile(file); @@ -102,7 +113,8 @@ async function loadScript(path: string): Promise