diff --git a/.gitignore b/.gitignore index cea5386..9364dab 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,6 @@ Thumbs.db # Cloudflare wrangler cache (tools/site deploys) .wrangler/ -# Kokoro TTS environment (tools/voice/make_voice.sh sets it up) +# Old Tam's voice environments (tools/voice/make_voice.sh sets one up; .venv was Kokoro, before 0.6.2) tools/voice/.venv/ +tools/voice/.venv-qwen/ diff --git a/assets/audio/CREDITS.md b/assets/audio/CREDITS.md index fdbb0e8..1a4103c 100644 --- a/assets/audio/CREDITS.md +++ b/assets/audio/CREDITS.md @@ -55,11 +55,14 @@ nednednerb (https://freesound.org/people/nednednerb/sounds/38250/) and (https://freesound.org/people/Dave%20Welsh/sounds/194364/). Thank you. ## Old Tam's voice -`voice/` is spoken by **Kokoro** (Kokoro-82M by hexgrad, Apache-2.0, -https://huggingface.co/hexgrad/Kokoro-82M) with its British "George" voice, -made a little older (deeper, slower, warmer) by `tools/voice/make_voice.py`. -The recordings are generated from our own text; rebuild them with -`tools/voice/make_voice.sh` after changing any line. +`voice/` is spoken by **Qwen3-TTS** (Qwen3-TTS-12Hz-1.7B by Alibaba's Qwen +team, Apache-2.0, https://huggingface.co/Qwen; run through mlx-audio, MIT). +His voice, a reedy old West Country farmer, was designed once from a written +description by Qwen3-TTS VoiceDesign (`tools/voice/old_tam/`), and every line +is spoken by cloning that recording. Each line's takes are checked against +the script by Whisper (OpenAI, MIT; mlx-whisper). The recordings are made from +our own text; rebuild them with `tools/voice/make_voice.sh` after changing any +line. (Up to 0.6.1 he was Kokoro-82M by hexgrad, Apache-2.0.) ## Cinderwild's own (CC0) `music/`, `ambience/` and `sfx/synth/` are synthesised from scratch by diff --git a/assets/audio/voice/hint_cold.ogg b/assets/audio/voice/hint_cold.ogg index 1055208..1decad9 100644 Binary files a/assets/audio/voice/hint_cold.ogg and b/assets/audio/voice/hint_cold.ogg differ diff --git a/assets/audio/voice/hint_cove.ogg b/assets/audio/voice/hint_cove.ogg index 628813e..4963f80 100644 Binary files a/assets/audio/voice/hint_cove.ogg and b/assets/audio/voice/hint_cove.ogg differ diff --git a/assets/audio/voice/hint_crab.ogg b/assets/audio/voice/hint_crab.ogg index f758b17..3525a01 100644 Binary files a/assets/audio/voice/hint_crab.ogg and b/assets/audio/voice/hint_crab.ogg differ diff --git a/assets/audio/voice/hint_died.ogg b/assets/audio/voice/hint_died.ogg index 04935f2..a045c92 100644 Binary files a/assets/audio/voice/hint_died.ogg and b/assets/audio/voice/hint_died.ogg differ diff --git a/assets/audio/voice/hint_diet.ogg b/assets/audio/voice/hint_diet.ogg index 3817bab..c88a6e0 100644 Binary files a/assets/audio/voice/hint_diet.ogg and b/assets/audio/voice/hint_diet.ogg differ diff --git a/assets/audio/voice/hint_freezing.ogg b/assets/audio/voice/hint_freezing.ogg index d66112a..b4bd4c5 100644 Binary files a/assets/audio/voice/hint_freezing.ogg and b/assets/audio/voice/hint_freezing.ogg differ diff --git a/assets/audio/voice/hint_hard_winter.ogg b/assets/audio/voice/hint_hard_winter.ogg index e967cb3..ad29c79 100644 Binary files a/assets/audio/voice/hint_hard_winter.ogg and b/assets/audio/voice/hint_hard_winter.ogg differ diff --git a/assets/audio/voice/hint_howl.ogg b/assets/audio/voice/hint_howl.ogg index b78f65b..9ddc4e7 100644 Binary files a/assets/audio/voice/hint_howl.ogg and b/assets/audio/voice/hint_howl.ogg differ diff --git a/assets/audio/voice/hint_hungry.ogg b/assets/audio/voice/hint_hungry.ogg index 2f9abe7..da55315 100644 Binary files a/assets/audio/voice/hint_hungry.ogg and b/assets/audio/voice/hint_hungry.ogg differ diff --git a/assets/audio/voice/hint_night.ogg b/assets/audio/voice/hint_night.ogg index 7d0881f..77efb46 100644 Binary files a/assets/audio/voice/hint_night.ogg and b/assets/audio/voice/hint_night.ogg differ diff --git a/assets/audio/voice/hint_rotten.ogg b/assets/audio/voice/hint_rotten.ogg index f150ad2..f1ae338 100644 Binary files a/assets/audio/voice/hint_rotten.ogg and b/assets/audio/voice/hint_rotten.ogg differ diff --git a/assets/audio/voice/hint_seam.ogg b/assets/audio/voice/hint_seam.ogg index d3eedb6..4f460be 100644 Binary files a/assets/audio/voice/hint_seam.ogg and b/assets/audio/voice/hint_seam.ogg differ diff --git a/assets/audio/voice/hint_sheltered.ogg b/assets/audio/voice/hint_sheltered.ogg index 5f0d838..b779af9 100644 Binary files a/assets/audio/voice/hint_sheltered.ogg and b/assets/audio/voice/hint_sheltered.ogg differ diff --git a/assets/audio/voice/hint_sick.ogg b/assets/audio/voice/hint_sick.ogg index eabe4f9..0346253 100644 Binary files a/assets/audio/voice/hint_sick.ogg and b/assets/audio/voice/hint_sick.ogg differ diff --git a/assets/audio/voice/hint_snow.ogg b/assets/audio/voice/hint_snow.ogg index 976dea8..658c5d7 100644 Binary files a/assets/audio/voice/hint_snow.ogg and b/assets/audio/voice/hint_snow.ogg differ diff --git a/assets/audio/voice/hint_stamina.ogg b/assets/audio/voice/hint_stamina.ogg index 2323417..5b6ec1b 100644 Binary files a/assets/audio/voice/hint_stamina.ogg and b/assets/audio/voice/hint_stamina.ogg differ diff --git a/assets/audio/voice/hint_storm.ogg b/assets/audio/voice/hint_storm.ogg index 5702d3a..8e0d690 100644 Binary files a/assets/audio/voice/hint_storm.ogg and b/assets/audio/voice/hint_storm.ogg differ diff --git a/assets/audio/voice/hint_wet.ogg b/assets/audio/voice/hint_wet.ogg index 647ac2c..d60a497 100644 Binary files a/assets/audio/voice/hint_wet.ogg and b/assets/audio/voice/hint_wet.ogg differ diff --git a/assets/audio/voice/hint_wolf_moon.ogg b/assets/audio/voice/hint_wolf_moon.ogg index 4bc7785..5dabe48 100644 Binary files a/assets/audio/voice/hint_wolf_moon.ogg and b/assets/audio/voice/hint_wolf_moon.ogg differ diff --git a/assets/audio/voice/lines.json b/assets/audio/voice/lines.json index 50b7452..e5d7614 100644 --- a/assets/audio/voice/lines.json +++ b/assets/audio/voice/lines.json @@ -1,44 +1,44 @@ { - "hint_cold": "91c5088e453fb0e6", - "hint_cove": "f7e5c0283b31e1cc", - "hint_crab": "283542496ecd180c", - "hint_died": "e865f8a63622b3c3", - "hint_diet": "3178b319c44e9fa0", - "hint_freezing": "48b1e505119a0fc9", - "hint_hard_winter": "5513aca1655bc278", + "hint_cold": "ac4139441b7a347a", + "hint_cove": "91eb0dcbc24afe5c", + "hint_crab": "3a88cd0772a7676d", + "hint_died": "7b60d478f6fdbcef", + "hint_diet": "b5e9600c2da38bdf", + "hint_freezing": "0a63a53be413d808", + "hint_hard_winter": "03e1316a475c3c43", "hint_howl": "05792dda3f3f0b60", - "hint_hungry": "ad76f22cb8e034a0", - "hint_night": "2a07d9c0bfae4bf9", - "hint_rotten": "98b69de03dbee089", - "hint_seam": "b353f46816d329b7", - "hint_sheltered": "ba6d6ff8bc4a1c80", - "hint_sick": "ec07963c46638ffc", - "hint_snow": "8f0ac7d093f73840", - "hint_stamina": "82bf5b903db7a29f", - "hint_storm": "f06bb82d15d991c8", - "hint_wet": "6aef86b85d67d7ac", - "hint_wolf_moon": "723b3dbd5439980c", - "tutorial_animals": "e82eb7e35e866812", - "tutorial_axe": "4bb51d6ea8b42840", - "tutorial_bed": "34cf4c842db48c19", - "tutorial_berries": "a7cd3805f11c8898", - "tutorial_campfire": "0a26bd897d1d8a90", - "tutorial_cook": "7164a8ab27a7821d", - "tutorial_diet": "fd47408ab4f2b5de", - "tutorial_finish": "36c07fc5a64c9d65", - "tutorial_fish": "32f2f3228c036136", - "tutorial_flax": "a87ff75ed8864ca6", - "tutorial_hut": "a10bfa289939dfb2", - "tutorial_look": "27acdf33905cc1a3", - "tutorial_mushroom": "79edcfb1afe18c8c", - "tutorial_pickaxe": "177e0474e4d0fda6", - "tutorial_rabbit": "6d429ab2cbdc7b19", - "tutorial_rod": "d610281e722e8fd3", - "tutorial_sleep": "e202cbf133500c2a", - "tutorial_sticks": "e526e30ddf8756a7", - "tutorial_torch": "68808fca608352fc", - "tutorial_tree": "1dfb2ab0491d962f", - "tutorial_weapon": "a17d1fe6d5067b8d", - "tutorial_wolf": "feb6bdda64e28161", - "tutorial_workbench": "1b6108ab18f05de9" + "hint_hungry": "c99c9fbc83506c43", + "hint_night": "04fc83670a1fd9e9", + "hint_rotten": "ecb8aecde8c6c762", + "hint_seam": "05531946c57cb97d", + "hint_sheltered": "7d3ebd27c230bc79", + "hint_sick": "b039c2449304ac53", + "hint_snow": "90a2e7f6735e4c83", + "hint_stamina": "e3e6cad6202abcaa", + "hint_storm": "477b250b6a9c97cb", + "hint_wet": "ebf9b034487bf92e", + "hint_wolf_moon": "d66b43dc8a23b5ed", + "tutorial_animals": "20b2eabd8de50d58", + "tutorial_axe": "c9a632910c8eaf19", + "tutorial_bed": "46f0130e86504f5b", + "tutorial_berries": "31d4fd1560fcf46f", + "tutorial_campfire": "6a7790be69235e80", + "tutorial_cook": "180b4f1413608105", + "tutorial_diet": "d44066db5adc0f78", + "tutorial_finish": "a9960edb587c3176", + "tutorial_fish": "7945d260ceff912b", + "tutorial_flax": "7e4f439fc7079272", + "tutorial_hut": "57c364fcde4bbff9", + "tutorial_look": "4998d8b95ff4fa8e", + "tutorial_mushroom": "ae582e11c9214891", + "tutorial_pickaxe": "0697d3610e8a70c5", + "tutorial_rabbit": "62544c47f9813cc4", + "tutorial_rod": "0aeae228c3e1e2ed", + "tutorial_sleep": "51fc176c04af2683", + "tutorial_sticks": "5a350695e62a28ef", + "tutorial_torch": "4bdb8837ac441e6e", + "tutorial_tree": "efcbe43e71059ba1", + "tutorial_weapon": "4d04fcd046eae1e2", + "tutorial_wolf": "b0725af812cc8d90", + "tutorial_workbench": "dd05c80fb8b3c3b8" } \ No newline at end of file diff --git a/assets/audio/voice/spoken.json b/assets/audio/voice/spoken.json index 1e52bd3..9e8c4b8 100644 --- a/assets/audio/voice/spoken.json +++ b/assets/audio/voice/spoken.json @@ -1,44 +1,45 @@ { - "hint_cold": "91c5088e453fb0e6", - "hint_cove": "f7e5c0283b31e1cc", - "hint_crab": "283542496ecd180c", - "hint_died": "e865f8a63622b3c3", - "hint_diet": "3178b319c44e9fa0", - "hint_freezing": "48b1e505119a0fc9", - "hint_hard_winter": "5513aca1655bc278", + "_voice": "c55ffaa0e19b744b", + "hint_cold": "ac4139441b7a347a", + "hint_cove": "91eb0dcbc24afe5c", + "hint_crab": "3a88cd0772a7676d", + "hint_died": "7b60d478f6fdbcef", + "hint_diet": "b5e9600c2da38bdf", + "hint_freezing": "0a63a53be413d808", + "hint_hard_winter": "03e1316a475c3c43", "hint_howl": "05792dda3f3f0b60", - "hint_hungry": "ad76f22cb8e034a0", - "hint_night": "2a07d9c0bfae4bf9", - "hint_rotten": "98b69de03dbee089", - "hint_seam": "b353f46816d329b7", - "hint_sheltered": "ba6d6ff8bc4a1c80", - "hint_sick": "ec07963c46638ffc", - "hint_snow": "8f0ac7d093f73840", - "hint_stamina": "82bf5b903db7a29f", - "hint_storm": "f06bb82d15d991c8", - "hint_wet": "6aef86b85d67d7ac", - "hint_wolf_moon": "723b3dbd5439980c", - "tutorial_animals": "e82eb7e35e866812", - "tutorial_axe": "4bb51d6ea8b42840", - "tutorial_bed": "34cf4c842db48c19", - "tutorial_berries": "a7cd3805f11c8898", - "tutorial_campfire": "b21eb5ddefb03f1a", - "tutorial_cook": "7164a8ab27a7821d", - "tutorial_diet": "fd47408ab4f2b5de", - "tutorial_finish": "36c07fc5a64c9d65", - "tutorial_fish": "32f2f3228c036136", - "tutorial_flax": "a87ff75ed8864ca6", - "tutorial_hut": "a10bfa289939dfb2", - "tutorial_look": "a88cca377c368962", - "tutorial_mushroom": "b641b54d2ef35404", - "tutorial_pickaxe": "177e0474e4d0fda6", - "tutorial_rabbit": "6d429ab2cbdc7b19", - "tutorial_rod": "d610281e722e8fd3", - "tutorial_sleep": "e202cbf133500c2a", - "tutorial_sticks": "e526e30ddf8756a7", - "tutorial_torch": "06ebc4c122fd8902", - "tutorial_tree": "58c2488e73c1fe4c", - "tutorial_weapon": "a17d1fe6d5067b8d", - "tutorial_wolf": "feb6bdda64e28161", - "tutorial_workbench": "1b6108ab18f05de9" + "hint_hungry": "c99c9fbc83506c43", + "hint_night": "04fc83670a1fd9e9", + "hint_rotten": "ecb8aecde8c6c762", + "hint_seam": "05531946c57cb97d", + "hint_sheltered": "7d3ebd27c230bc79", + "hint_sick": "b039c2449304ac53", + "hint_snow": "90a2e7f6735e4c83", + "hint_stamina": "e3e6cad6202abcaa", + "hint_storm": "477b250b6a9c97cb", + "hint_wet": "ebf9b034487bf92e", + "hint_wolf_moon": "d66b43dc8a23b5ed", + "tutorial_animals": "20b2eabd8de50d58", + "tutorial_axe": "c9a632910c8eaf19", + "tutorial_bed": "46f0130e86504f5b", + "tutorial_berries": "31d4fd1560fcf46f", + "tutorial_campfire": "6a7790be69235e80", + "tutorial_cook": "180b4f1413608105", + "tutorial_diet": "d44066db5adc0f78", + "tutorial_finish": "a9960edb587c3176", + "tutorial_fish": "7945d260ceff912b", + "tutorial_flax": "7e4f439fc7079272", + "tutorial_hut": "57c364fcde4bbff9", + "tutorial_look": "2345ea0b154d4014", + "tutorial_mushroom": "ae582e11c9214891", + "tutorial_pickaxe": "0697d3610e8a70c5", + "tutorial_rabbit": "62544c47f9813cc4", + "tutorial_rod": "0aeae228c3e1e2ed", + "tutorial_sleep": "51fc176c04af2683", + "tutorial_sticks": "5a350695e62a28ef", + "tutorial_torch": "4bdb8837ac441e6e", + "tutorial_tree": "efcbe43e71059ba1", + "tutorial_weapon": "4d04fcd046eae1e2", + "tutorial_wolf": "b0725af812cc8d90", + "tutorial_workbench": "dd05c80fb8b3c3b8" } \ No newline at end of file diff --git a/assets/audio/voice/tutorial_animals.ogg b/assets/audio/voice/tutorial_animals.ogg index 35d49af..71bb88b 100644 Binary files a/assets/audio/voice/tutorial_animals.ogg and b/assets/audio/voice/tutorial_animals.ogg differ diff --git a/assets/audio/voice/tutorial_axe.ogg b/assets/audio/voice/tutorial_axe.ogg index c475cc7..f7f12e1 100644 Binary files a/assets/audio/voice/tutorial_axe.ogg and b/assets/audio/voice/tutorial_axe.ogg differ diff --git a/assets/audio/voice/tutorial_bed.ogg b/assets/audio/voice/tutorial_bed.ogg index c3c3ba5..7e78fad 100644 Binary files a/assets/audio/voice/tutorial_bed.ogg and b/assets/audio/voice/tutorial_bed.ogg differ diff --git a/assets/audio/voice/tutorial_berries.ogg b/assets/audio/voice/tutorial_berries.ogg index becf48c..3f082f0 100644 Binary files a/assets/audio/voice/tutorial_berries.ogg and b/assets/audio/voice/tutorial_berries.ogg differ diff --git a/assets/audio/voice/tutorial_campfire.ogg b/assets/audio/voice/tutorial_campfire.ogg index 4a7d91c..41124f9 100644 Binary files a/assets/audio/voice/tutorial_campfire.ogg and b/assets/audio/voice/tutorial_campfire.ogg differ diff --git a/assets/audio/voice/tutorial_cook.ogg b/assets/audio/voice/tutorial_cook.ogg index 3120847..15c37e0 100644 Binary files a/assets/audio/voice/tutorial_cook.ogg and b/assets/audio/voice/tutorial_cook.ogg differ diff --git a/assets/audio/voice/tutorial_diet.ogg b/assets/audio/voice/tutorial_diet.ogg index d1cc236..d59a5c9 100644 Binary files a/assets/audio/voice/tutorial_diet.ogg and b/assets/audio/voice/tutorial_diet.ogg differ diff --git a/assets/audio/voice/tutorial_finish.ogg b/assets/audio/voice/tutorial_finish.ogg index 8ee1ed2..8b6c568 100644 Binary files a/assets/audio/voice/tutorial_finish.ogg and b/assets/audio/voice/tutorial_finish.ogg differ diff --git a/assets/audio/voice/tutorial_fish.ogg b/assets/audio/voice/tutorial_fish.ogg index ddc86a4..6276d2b 100644 Binary files a/assets/audio/voice/tutorial_fish.ogg and b/assets/audio/voice/tutorial_fish.ogg differ diff --git a/assets/audio/voice/tutorial_flax.ogg b/assets/audio/voice/tutorial_flax.ogg index 530858a..af58549 100644 Binary files a/assets/audio/voice/tutorial_flax.ogg and b/assets/audio/voice/tutorial_flax.ogg differ diff --git a/assets/audio/voice/tutorial_hut.ogg b/assets/audio/voice/tutorial_hut.ogg index 6d9933e..3d975ec 100644 Binary files a/assets/audio/voice/tutorial_hut.ogg and b/assets/audio/voice/tutorial_hut.ogg differ diff --git a/assets/audio/voice/tutorial_look.ogg b/assets/audio/voice/tutorial_look.ogg index da3e189..484548c 100644 Binary files a/assets/audio/voice/tutorial_look.ogg and b/assets/audio/voice/tutorial_look.ogg differ diff --git a/assets/audio/voice/tutorial_mushroom.ogg b/assets/audio/voice/tutorial_mushroom.ogg index a54d8f5..7858cfb 100644 Binary files a/assets/audio/voice/tutorial_mushroom.ogg and b/assets/audio/voice/tutorial_mushroom.ogg differ diff --git a/assets/audio/voice/tutorial_pickaxe.ogg b/assets/audio/voice/tutorial_pickaxe.ogg index ee65884..d36c11e 100644 Binary files a/assets/audio/voice/tutorial_pickaxe.ogg and b/assets/audio/voice/tutorial_pickaxe.ogg differ diff --git a/assets/audio/voice/tutorial_rabbit.ogg b/assets/audio/voice/tutorial_rabbit.ogg index f3d1d0b..28acf2b 100644 Binary files a/assets/audio/voice/tutorial_rabbit.ogg and b/assets/audio/voice/tutorial_rabbit.ogg differ diff --git a/assets/audio/voice/tutorial_rod.ogg b/assets/audio/voice/tutorial_rod.ogg index 57ca72d..cf12f15 100644 Binary files a/assets/audio/voice/tutorial_rod.ogg and b/assets/audio/voice/tutorial_rod.ogg differ diff --git a/assets/audio/voice/tutorial_sleep.ogg b/assets/audio/voice/tutorial_sleep.ogg index 2790310..5a5280e 100644 Binary files a/assets/audio/voice/tutorial_sleep.ogg and b/assets/audio/voice/tutorial_sleep.ogg differ diff --git a/assets/audio/voice/tutorial_sticks.ogg b/assets/audio/voice/tutorial_sticks.ogg index de34412..be8eca0 100644 Binary files a/assets/audio/voice/tutorial_sticks.ogg and b/assets/audio/voice/tutorial_sticks.ogg differ diff --git a/assets/audio/voice/tutorial_torch.ogg b/assets/audio/voice/tutorial_torch.ogg index 46ed463..cb5b5f0 100644 Binary files a/assets/audio/voice/tutorial_torch.ogg and b/assets/audio/voice/tutorial_torch.ogg differ diff --git a/assets/audio/voice/tutorial_tree.ogg b/assets/audio/voice/tutorial_tree.ogg index 5876b69..1782940 100644 Binary files a/assets/audio/voice/tutorial_tree.ogg and b/assets/audio/voice/tutorial_tree.ogg differ diff --git a/assets/audio/voice/tutorial_weapon.ogg b/assets/audio/voice/tutorial_weapon.ogg index 173a0e2..897d308 100644 Binary files a/assets/audio/voice/tutorial_weapon.ogg and b/assets/audio/voice/tutorial_weapon.ogg differ diff --git a/assets/audio/voice/tutorial_wolf.ogg b/assets/audio/voice/tutorial_wolf.ogg index dad6ce4..971b057 100644 Binary files a/assets/audio/voice/tutorial_wolf.ogg and b/assets/audio/voice/tutorial_wolf.ogg differ diff --git a/assets/audio/voice/tutorial_workbench.ogg b/assets/audio/voice/tutorial_workbench.ogg index 88999a6..85bbc6a 100644 Binary files a/assets/audio/voice/tutorial_workbench.ogg and b/assets/audio/voice/tutorial_workbench.ogg differ diff --git a/data/hints.json b/data/hints.json index 2828228..7a554fe 100644 --- a/data/hints.json +++ b/data/hints.json @@ -3,42 +3,42 @@ "_doc": "T4 (docs/TUTORIAL_DESIGN.md §5): one-off tips in normal worlds, each shown once per player (user://hints.json), in list order when several are due. 'when' is kind:value, with | for alternatives: status (a survival status, by prefix), weather (a state), has (an item in your bag), event (howl, died), worldlet (where you are on the journey), or a plain check (night, stamina_low, wolf_moon, hard_winter, seam: holding the antler pickaxe with the cliff tunnel's seam still to dig). 'hold' is how long it must stay true first (seconds, default 1).", "hints": [ {"id": "died", "when": "event:died", "title": "Back on your feet", - "text": "Dying's not the end out here. Whatever you carried is waiting where you fell, so go back for it before anything else. Sleep in a bed once and that's where you'll wake."}, + "text": "Dying's not the end out here, don't you fret. Whatever you were carrying is waiting where you fell, so go back for it first. Sleep in a bed once, and that's where you'll wake."}, {"id": "freezing", "when": "status:Freezing", "title": "Freezing", - "text": "You're freezing, and that eats at your health. Get to a fire or under a roof, now. A cloak helps too."}, + "text": "You're freezing, and that'll eat away at your health! Get to a fire or under a roof, now. A cloak helps too."}, {"id": "cold", "when": "status:Cold", "hold": 3, "title": "Getting cold", - "text": "Feel that chill? A fire warms you, a roof and walls keep the wind off, and a hide cloak buys you time."}, + "text": "Feel that chill? A fire warms you up, a roof and walls keep the wind off, and a hide cloak buys you a bit of time."}, {"id": "sick", "when": "status:Food poisoning", "title": "Bad food", - "text": "Raw meat or something past its best: your belly's turned. It passes. Rest, don't eat for a bit, and cook your meat next time."}, + "text": "Ooh, your belly's turned. Raw meat, or something past its best, I'd wager. It'll pass. Rest a while, don't eat for a bit, and cook your meat next time."}, {"id": "rotten", "when": "has:rotten_food", "title": "Food goes off", - "text": "Food doesn't keep forever. Eat the fresh stuff first, smoke meat and fish on a rack, or salt it. Rotten food's no good to anyone: throw it out."}, + "text": "Food doesn't keep forever, you know. Eat the fresh stuff first, smoke your meat and fish on a rack, or salt it. Rotten food's no good to anyone, so throw it out."}, {"id": "wet", "when": "status:Wet", "hold": 2, "title": "Soaked", - "text": "Wet clothes steal your warmth. Stand under a roof or by a fire and you'll dry off."}, + "text": "You're soaked through! Wet clothes steal your warmth. Stand under a roof or by a fire and you'll dry off."}, {"id": "hungry", "when": "status:Hungry", "hold": 2, "title": "Hungry", - "text": "Your belly's empty. Berries and mushrooms tide you over, but cooked meat and fish fill you up for longer."}, + "text": "Your belly's empty. Berries and mushrooms will tide you over, but cooked meat and fish fill you up for longer."}, {"id": "diet", "when": "status:Poor diet|Malnourished", "hold": 2, "title": "Mix it up", - "text": "Eat the same thing every day and your body notices. Watch the three diet bars: meat, fish and greens. Keep them all topped up and you'll be stronger for it."}, + "text": "Eat the same thing every day and your body notices, it does. Watch them three diet bars: meat, fish and greens. Keep them all topped up and you'll be stronger for it."}, {"id": "howl", "when": "event:howl", "title": "Wolves", "text": "Hear that? Wolves have moved into the hills. They'll come hunting soon after dark. Walls, a door and a fire are your friends."}, {"id": "wolf_moon", "when": "wolf_moon", "title": "The red moon", - "text": "A red moon. The pack's bolder tonight and the big one leads them. Stay behind a good door, or stand by a fire with a weapon ready."}, + "text": "A red moon, look. The pack's bolder tonight, and the big one leads them. Stay behind a good door, or stand by a fire with a weapon ready."}, {"id": "night", "when": "night", "hold": 5, "title": "Nightfall", - "text": "Dark and cold till morning. Keep near a fire or carry a torch. A bed under a roof lets you sleep through it."}, + "text": "Dark and cold till morning, now. Keep near a fire or carry a torch. A bed under a roof lets you sleep right through it."}, {"id": "hard_winter", "when": "hard_winter", "hold": 3, "title": "Hard winter", - "text": "A hard winter's come in: a few days of bitter cold, and blizzards that freeze you even by day. Stock wood and food, and stay close to home."}, + "text": "A hard winter's come in. A few days of bitter cold, and blizzards that'll freeze you even by day. Stock up on wood and food, and stay close to home."}, {"id": "snow", "when": "weather:snow|blizzard", "hold": 3, "title": "Snow", - "text": "Snow's falling. It's colder out in it, and heavy snow piles up on roofs. Keep your fire fed."}, + "text": "Snow's falling. It's colder out in it, and heavy snow piles up on your roofs. Keep your fire fed."}, {"id": "storm", "when": "weather:storm", "hold": 3, "title": "Storm", - "text": "A storm. Lightning goes for the trees, so don't shelter under one. Your buildings are safe enough."}, + "text": "A storm's coming in. Lightning goes for the trees, so don't you go sheltering under one. Your buildings are safe enough."}, {"id": "stamina", "when": "stamina_low", "title": "Out of breath", - "text": "You're spent. Stop swinging and running a moment and your stamina comes back. Rested by a fire, it comes back faster."}, + "text": "You're all puffed out. Stop swinging and running a moment, and your stamina comes back. Rested by a fire, it comes back faster."}, {"id": "sheltered", "when": "status:Sheltered", "hold": 3, "title": "A proper shelter", - "text": "Roof and walls: the weather can't touch you here. Sit by a fire in here a little while and you'll be Rested."}, + "text": "Roof and walls, that's the way. The weather can't touch you in here. Sit by a fire inside a little while and you'll be Rested."}, {"id": "seam", "when": "seam", "hold": 3, "title": "The cracked seam", - "text": "That antler pick bites rock iron only scratches. Out on the clifftop, near the edge, there's a seam of cracked rock, and a way down to the sea behind it if you dig. Take wood to prop it as you go, and lights: it's black as pitch in there."}, + "text": "That antler pick bites rock that iron only scratches. Out on the clifftop, near the edge, there's a seam of cracked rock, and a way down to the sea behind it if you dig. Take wood to prop it as you go, and lights. It's black as pitch down there."}, {"id": "cove", "when": "worldlet:cove", "hold": 3, "title": "The Cove", - "text": "You made it down. Sand, salt and sea fish. Fish off the end of the pier: mackerel bite close in, cod further out where it's deep. The sea cave takes you back up the tunnel, and home's waiting at the top. Mind the gulls: they'll have any food you leave on the sand, and fish off a smoking rack. And at dusk the crabs come up the beach: give them room, or they pinch."}, + "text": "Well, you made it down! Sand, salt and sea fish. Fish off the end of the pier: mackerel bite close in, and cod further out where it's deep. The sea cave takes you back up the tunnel to home. Watch out for the gulls: they'll have any food you leave on the sand, and fish off a smoking rack. And at dusk the crabs come up the beach, so give them room, or they'll pinch."}, {"id": "crab", "when": "has:crab_meat", "hold": 2, "title": "Crab", - "text": "Two lots of crab meat cook into a good crab supper at a campfire. Raw, it's as risky as raw fish."} + "text": "Two lots of crab meat cook up into a good crab supper at a campfire. Raw, it's as risky as raw fish."} ] } diff --git a/data/tutorial.json b/data/tutorial.json index ba8909a..95a3561 100644 --- a/data/tutorial.json +++ b/data/tutorial.json @@ -30,7 +30,7 @@ "id": "look", "chapter": "tools", "title": "Look around", - "text": "Awake at last! Washed up on my island, have you. Stretch your legs: WASD to walk, the mouse to look. Shift if you're in a hurry.", + "text": "Ha! Awake at last! Washed up on my island, eh? Well, stretch your legs. Use WASD to move, the mouse to look around, and Shift to run.", "goal": { "walk": 10 } @@ -39,7 +39,7 @@ "id": "sticks", "chapter": "tools", "title": "Gather sticks and stones", - "text": "Empty hands are no use here. Look at a stick or a stone on the ground and press E. Three of each, mind.", + "text": "Now then! Empty hands are no use to anyone out here. Look at a stick or a stone on the ground and press E to pick it up. Why don't you go ahead and grab three of each.", "marker": { "nearest": "pickup:stick|stone" }, @@ -60,7 +60,7 @@ "id": "flax", "chapter": "tools", "title": "Pick wild flax", - "text": "See the pale, tall plants? Wild flax. Pick some. The fibre makes torches, bows and a decent bed.", + "text": "See them tall, pale plants? That's wild flax. Pick a few of those. The fibre makes bows, a decent bed, and torches. You'll need a torch before long. It gets pretty dark here.", "marker": { "nearest": "pickup:fiber" }, @@ -73,7 +73,7 @@ "id": "axe", "chapter": "tools", "title": "Craft a stone axe", - "text": "Now an axe. Two sticks and two flint: flint lies about, more by the water. Tab opens your pack, and the recipe's in there.", + "text": "Right, now you'll be wanting an axe. That's made from two sticks and two flint. Flint can be found lying all over the place, slightly darker than a stone. You'll find more by the water. If you press Tab to open your pack, you'll find the recipe in there.", "marker": { "nearest": "pickup:flint", "unless_have": { @@ -88,7 +88,7 @@ "id": "tree", "chapter": "tools", "title": "Chop down a tree", - "text": "Axe in hand (the number keys), hold the left button at a trunk and keep at it. A tree gives wood, and sticks when it falls.", + "text": "Got your axe? Good. Pick it with the number keys so it's in your hand, then click the left mouse button at a tree trunk a few times. Down it comes, and you'll get wood, and a few sticks besides.", "marker": { "nearest": "tree" }, @@ -100,7 +100,7 @@ "id": "pickaxe", "chapter": "tools", "title": "Craft a pickaxe and break a boulder", - "text": "Rocks are next. A pickaxe takes two sticks, three stone and a flint. Then go and break a boulder for stone and flint of your own.", + "text": "Now for the rocks. Make yourself a pickaxe, from two sticks, three stone and a flint. Then go and give one of them big boulders a good few whacks, and you'll have stone and flint of your own.", "marker": { "nearest": "rock" }, @@ -119,7 +119,7 @@ "id": "campfire", "chapter": "food", "title": "Build a campfire", - "text": "Five stone and four sticks make a campfire. Pick it in your pack, then place it with the left button (R turns it). Fire gives light, warmth and somewhere to cook. Feed it wood with E.", + "text": "Now, a fire. Five stone and four sticks make a campfire. Pick it in your pack, then click the left mouse button to put it down. R turns it round, if you're fussy. A fire gives you light, warmth, and somewhere to cook. Keep it fed with wood: look at it and press E.", "goal": { "placed": "campfire" } @@ -128,7 +128,7 @@ "id": "berries", "chapter": "food", "title": "Pick berries and eat some", - "text": "Those red berries on the bushes are safe. Pick some with E, then hold them and press the right button to eat.", + "text": "Hungry? Them red berries on the bushes are safe enough. Pick some with E, then hold them with the number keys and click the right mouse button to eat. Go on, have a few.", "goal": { "all": [ { @@ -147,7 +147,7 @@ "id": "mushroom", "chapter": "food", "title": "Roast a mushroom", - "text": "Mushrooms grow under the trees. Raw ones are all right; roasted at the fire, much better. Find one and roast it (stand by the fire and open your pack).", + "text": "Mushrooms grow in the shade, under the trees. Raw ones are all right, but roasted at the fire they're a sight better. Find one, then stand by your fire and open your pack to roast it.", "goal": { "all": [ { @@ -169,7 +169,7 @@ "id": "diet", "chapter": "food", "title": "Mind your diet", - "text": "See the three Diet bars, bottom left? Meat, Fish, Greens. Each food fills one. Let one run low and you stop healing; let two, and you'll waste away on a full belly. Keep all three up and you heal faster. Press Enter when you've got that.", + "text": "Now, see them three Diet bars, bottom left? Meat, fish and greens. Each food fills one of them. Let one run low and you'll stop healing. Let two run low, and you'll waste away on a full belly. Keep all three topped up and you'll heal faster. Press Enter when you've got all that.", "goal": { "continue": true } @@ -178,7 +178,7 @@ "id": "workbench", "chapter": "hunt", "title": "Build a workbench", - "text": "Better tools want a workbench nearby. Ten wood and two flint. Chop a few more trees if you're short.", + "text": "Better tools want a workbench close by. That's ten wood and two flint. If you're short, go and chop down a few more trees. Then put it down near your fire.", "goal": { "placed": "workbench" } @@ -187,7 +187,7 @@ "id": "weapon", "chapter": "hunt", "title": "Make a weapon", - "text": "You'll not catch your supper with your bare hands. A club is four wood. A spear, at the bench, reaches further.", + "text": "You'll not catch your supper with your bare hands, will you? A club is four wood, and you can make that anywhere. A spear reaches further, but you'll need to be at the bench for that one.", "goal": { "any": [ { @@ -203,7 +203,7 @@ "id": "rabbit", "chapter": "hunt", "title": "Hunt a rabbit", - "text": "There, rabbits! They bolt if you run at them. Walk up quiet, weapon in hand, and strike.", + "text": "There! Rabbits! Run at them and they'll bolt, so creep up quiet, weapon in hand, and give them a whack.", "goal": { "killed": "rabbit" }, @@ -222,7 +222,7 @@ "id": "cook", "chapter": "hunt", "title": "Butcher it and cook the meat", - "text": "E on the carcass to butcher it. Raw meat turns your stomach; cooked at the fire it heals you and keeps you Well fed.", + "text": "Now look at it and press E to butcher it. Don't you go eating that meat raw, it'll turn your stomach. Cook it at the fire, and it'll heal you up and keep you Well fed.", "goal": { "all": [ { @@ -241,7 +241,7 @@ "id": "animals", "chapter": "hunt", "title": "Know your animals", - "text": "Deer are quick: a bow's your friend there. Boars are calm until you hit one, then they come for you. Wolves you'll meet soon enough. Press Enter.", + "text": "Deer are quick, so a bow's your friend there. Boars are calm enough until you hit one, then they'll come right for you. And wolves? Well, you'll meet them soon enough. Press Enter when you're ready.", "goal": { "continue": true } @@ -250,7 +250,7 @@ "id": "rod", "chapter": "fish", "title": "Make a fishing rod", - "text": "Two sticks, three fibre and a flint, at the workbench.", + "text": "Fancy a bit of fish? Make yourself a rod at the workbench, from two sticks, three fibre and a flint.", "goal": { "crafted": "fishing_rod" } @@ -259,7 +259,7 @@ "id": "fish", "chapter": "fish", "title": "Catch a fish and grill it", - "text": "Down to the lake. Rod in hand, the left button casts. When the float dips, click quick, before it gets away. Grill what you catch at the fire.", + "text": "Down to the lake with you. Rod in hand, click the left mouse button to cast. When the float dips, click again, quick as you can, before it gets away. Then grill what you catch at the fire.", "goal": { "all": [ { @@ -285,7 +285,7 @@ "id": "hut", "chapter": "night", "title": "Build a little hut", - "text": "Nights get cold, and things prowl. Build a hut: I've marked out a good spot by your fire, pale as a ghost. Floors, walls and a door, then the roof, all from the bench. Put a piece near its place in the outline and it'll go right in. Done when you can stand inside out of the weather.", + "text": "Nights get cold out here, and things prowl about. You'll want a hut. I've marked out a good spot by your fire, pale as a ghost. Floors, walls and a door, then the roof, all made at the bench. Put each piece near its place in the outline and it'll go right in. You're done when you can stand inside, out of the weather.", "goal": { "shelter": true }, @@ -300,7 +300,7 @@ "id": "bed", "chapter": "night", "title": "Make a bed and sleep spot", - "text": "Eight wood and six fibre make a bed. Put it inside, then E on it: that's where you'll wake if anything happens to you.", + "text": "Now a bed. That's eight wood and six fibre. Put it inside your hut, then look at it and press E. That's where you'll wake up if anything happens to you.", "goal": { "all": [ { @@ -316,7 +316,7 @@ "id": "torch", "chapter": "night", "title": "Night is coming: make a torch", - "text": "There, the sun's going down. It gets dark and cold. A torch (a stick and two fibre) lights the way and keeps you a little warm.", + "text": "There, the sun's going down. It gets dark and cold, mind. Make yourself a torch, from a stick and two fibre. It lights the way, and keeps you a little warm besides.", "goal": { "crafted": "torch" }, @@ -328,7 +328,7 @@ "id": "wolf", "chapter": "night", "title": "Wolves", - "text": "Hush. See it, at the edge of the light? Wolves. From the fourth night they hunt. They won't come close to a lit fire, a torch makes them think twice, and they can't bite through walls. Just watch.", + "text": "Hush now. See it, there, at the edge of the light? A wolf. From the fourth night they hunt. They won't come near a lit fire, a torch makes them think twice, and they can't bite through walls. Just you watch.", "goal": { "demo_wolf": true }, @@ -340,7 +340,7 @@ "id": "sleep", "chapter": "night", "title": "Sleep till morning", - "text": "Off to bed with you. You can only sleep at night, under a roof with walls round you, and not with wolves about.", + "text": "Off to bed with you. You can only sleep at night, under a roof with walls round you, and not with wolves about. Look at your bed and press E.", "goal": { "slept": true }, @@ -351,6 +351,6 @@ ], "finish": { "title": "You'll do", - "text": "That's the island, near enough: gather, craft, eat a bit of everything, keep warm and dry, and be indoors or by a fire when the wolves are about. E picks up and uses, Tab is your pack, X takes a thing down, R turns what you're placing, the right button eats or wears. Off you go, then." + "text": "Well, you'll do. That's the island, near enough. Gather, craft, eat a bit of everything, keep warm and dry, and be indoors or by a fire when the wolves are about. Remember: E picks up and uses, Tab is your pack, X takes a thing down, R turns what you're placing, and the right mouse button eats or wears. Off you go, then." } } diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e87308d..e148b41 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -100,7 +100,7 @@ phases M2.0-M2.7). - Audio, music, weather. *Done (0.2.0-0.2.1):* mood music (calm, night, storm, danger, boss) that crossfades with what's happening, ambience (CC0 birdsong, crickets, owls, fire), sound effects for every action (Kenney - CC0 plus synthesised), creature voices, Old Tam's spoken lines (Kokoro). + CC0 plus synthesised), creature voices, Old Tam's spoken lines (Kokoro, then Qwen3-TTS from 0.6.2). Sources and licences: `assets/audio/CREDITS.md`; tools in `tools/audio/` and `tools/voice/`. - Help: the player guide on the download site (`tools/site/`), written diff --git a/tests/test_hints.gd b/tests/test_hints.gd index e8f14ff..48c9022 100644 --- a/tests/test_hints.gd +++ b/tests/test_hints.gd @@ -95,7 +95,7 @@ func _body() -> void: sv.wet = 60.0 await _for(3.0) check(got == ["wet"], "soaked: the wet tip") - check(card.is_showing() and card._title.text == "Soaked" and card._text.text.begins_with("\"Wet clothes"), "the card shows it, in Old Tam's words") + check(card.is_showing() and card._title.text == "Soaked" and card._text.text.begins_with("\"You're soaked"), "the card shows it, in Old Tam's words") sv.wet = 0.0 sv.hunger = 10.0 diff --git a/tools/voice/make_voice.py b/tools/voice/make_voice.py index 959a048..2e86945 100644 --- a/tools/voice/make_voice.py +++ b/tools/voice/make_voice.py @@ -1,23 +1,33 @@ """Old Tam's voice: every line he says (the tutorial's steps and goodbye in -data/tutorial.json, his tips in data/hints.json), spoken by Kokoro (Kokoro-82M, -Apache-2.0, hexgrad) with its British "George" voice, then made a little older: -deeper, slower and warmer. Output: assets/audio/voice/.ogg plus -lines.json, the text each file was made from (the game only plays a file -whose text still matches, so an edited line goes quiet rather than saying -the old words; tests/test_tutorial.gd checks they're all up to date). +data/tutorial.json, his tips in data/hints.json), in his own voice: Qwen3-TTS +(Alibaba's Qwen team, Apache-2.0) cloning tools/voice/old_tam/reference.wav, +a reedy old West Country farmer that Qwen3's VoiceDesign made from a written +description (old_tam/voice.json). Cloning that one recording, rather than +describing him again for each line, keeps him the same man throughout. -Run it with tools/voice/make_voice.sh (sets up the Kokoro environment once). -Only lines whose text changed are re-made; --all re-makes everything. +Each line is recorded TAKES times; Whisper (mlx-whisper) writes down what +each take says, and the take whose words best match the line is kept (a take +that drops or garbles words loses). Output: assets/audio/voice/.ogg, +lines.json (the written text each file was made from: the game only plays a +file whose text still matches, so an edited line goes quiet rather than +saying the old words; tests/test_tutorial.gd checks they're all up to date) +and spoken.json (what each one says). + +Run it with tools/voice/make_voice.sh (sets up the environment once, on an +Apple-silicon Mac: MLX). Only lines whose text, or how they're spoken, changed +are re-made; --all re-makes everything; --takes N for more takes a line. """ -import hashlib, json, os, re, subprocess, sys, tempfile +import difflib, hashlib, json, os, re, subprocess, sys, tempfile ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) OUT = os.path.join(ROOT, "assets", "audio", "voice") -VOICE = "bm_george" -SPEED = 0.9 -# Older: down about 1.4 semitones (and so a touch slower), then a little -# warmth and the top taken off. -AGE = "asetrate=24000*0.92,aresample=24000,atempo=1.04,lowpass=f=6500,bass=g=2:f=180" +HERE = os.path.dirname(os.path.abspath(__file__)) +VOICE = json.load(open(os.path.join(HERE, "old_tam", "voice.json"))) +MODEL = VOICE["model"] +REF_AUDIO = os.path.join(HERE, "old_tam", VOICE["reference_audio"]) +REF_TEXT = VOICE["reference_text"] +WHISPER = "mlx-community/whisper-large-v3-turbo" +TAKES = 3 def lines(): @@ -45,19 +55,24 @@ def spoken(text): s = s.replace(" · ", ", ").replace("&", "and") s = re.sub(r"\bLMB\b", "left click", s) s = re.sub(r"\bRMB\b", "right click", s) - s = re.sub(r"\bWASD\b", "W, A, S and D", s) # (Gitea #3: it said "Wasad") + # (Gitea #3: spelt out, "W, A, S and D" comes out rushed; the screen shows the keys.) + s = re.sub(r"\bWASD\b", "the keys", s) s = re.sub(r"\s+,", ",", s) return s -def _changed_by_new_rules(text): - """Whether a line has something the spoken() rules added since the last - full recording (0.6.2): "WASD".""" - return re.search(r"\bWASD\b", text) is not None +def _words(s): + return re.sub(r"[^a-z' ]", " ", s.lower().replace("-", " ")).split() + + +def match(want, heard): + """How closely what Whisper heard matches the line (0..1).""" + return difflib.SequenceMatcher(None, _words(want), _words(heard)).ratio() def main(): redo_all = "--all" in sys.argv + takes = int(sys.argv[sys.argv.index("--takes") + 1]) if "--takes" in sys.argv else TAKES os.makedirs(OUT, exist_ok=True) manifest_path = os.path.join(OUT, "lines.json") old = json.load(open(manifest_path)) if os.path.exists(manifest_path) else {} @@ -66,38 +81,57 @@ def main(): # key: that's what the game checks against the screen.) said_path = os.path.join(OUT, "spoken.json") said = json.load(open(said_path)) if os.path.exists(said_path) else {} + voice_key = key(json.dumps(VOICE, sort_keys=True)) + new_voice = said.get("_voice") != voice_key todo = [(i, t) for i, t in lines() - if redo_all or old.get(i) != key(t) or said.get(i, key(spoken(t))) != key(spoken(t)) + if redo_all or new_voice or old.get(i) != key(t) or said.get(i) != key(spoken(t)) or not os.path.exists(os.path.join(OUT, i + ".ogg"))] - if not said: - # (First run with spoken.json: lines spoken differently now than when - # they were recorded can't be told apart, so re-record the ones whose - # spoken form isn't just their text.) - todo += [(i, t) for i, t in lines() if spoken(t) != t and (i, t) not in todo and _changed_by_new_rules(t)] keep = {i for i, _ in lines()} for f in os.listdir(OUT): # lines that no longer exist if f.endswith(".ogg") and f[:-4] not in keep: os.remove(os.path.join(OUT, f)) + report = [] if todo: import numpy as np import soundfile as sf - from kokoro import KPipeline - pipe = KPipeline(lang_code="b", repo_id="hexgrad/Kokoro-82M") - for i, text in todo: - audio = np.concatenate([a for _, _, a in pipe(spoken(text), voice=VOICE, speed=SPEED)]) - tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name - sf.write(tmp, audio, 24000) + import mlx_whisper + from mlx_audio.tts.utils import load_model + model = load_model(MODEL) + sr = getattr(model, "sample_rate", 24000) + tmpdir = tempfile.mkdtemp() + for n, (i, text) in enumerate(todo, 1): + say = spoken(text) + best = None + for k in range(takes): + res = list(model.generate(text=say, ref_audio=REF_AUDIO, ref_text=REF_TEXT)) + audio = np.concatenate([np.array(r.audio) for r in res]) + wav = os.path.join(tmpdir, "%s_%d.wav" % (i, k)) + sf.write(wav, audio, sr) + heard = mlx_whisper.transcribe(wav, path_or_hf_repo=WHISPER, language="en")["text"].strip() + score = match(say, heard) + if best is None or score > best[0]: + best = (score, wav, heard, len(audio) / sr) + if score >= 0.98: + break # (word for word: no need for another) + score, wav, heard, secs = best path = os.path.join(OUT, i + ".ogg") # (ffmpeg's own Vorbis encoder only writes stereo) - subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", tmp, "-af", AGE + ",apad=pad_dur=0.25", "-ac", "2", - "-c:a", "vorbis", "-strict", "-2", "-q:a", "4", path], check=True) - os.remove(tmp) + subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", wav, "-af", "apad=pad_dur=0.25", "-ac", "2", + "-c:a", "vorbis", "-strict", "-2", "-q:a", "5", path], check=True) old[i] = key(text) - print(" %s (%.1f s)" % (i, len(audio) / 24000 / 0.92 / 1.04)) + said[i] = key(say) + flag = "" if score >= 0.9 else " <-- check this one: heard \"%s\"" % heard + report.append((score, i, heard)) + print(" [%d/%d] %s (%.1f s, words %.0f%%)%s" % (n, len(todo), i, secs, score * 100, flag)) manifest = {i: key(t) for i, t in lines()} json.dump(manifest, open(manifest_path, "w"), indent="\t", sort_keys=True) - json.dump({i: key(spoken(t)) for i, t in lines()}, open(said_path, "w"), indent="\t", sort_keys=True) + out_said = {i: said.get(i, key(spoken(t))) for i, t in lines()} + out_said["_voice"] = voice_key + json.dump(out_said, open(said_path, "w"), indent="\t", sort_keys=True) print("%d lines, %d made now -> %s" % (len(manifest), len(todo), os.path.relpath(OUT, ROOT))) + weak = [r for r in report if r[0] < 0.9] + if weak: + print("%d to listen to (the words didn't quite match): %s" % (len(weak), ", ".join(r[1] for r in weak))) if __name__ == "__main__": diff --git a/tools/voice/make_voice.sh b/tools/voice/make_voice.sh index 67182e1..11394de 100755 --- a/tools/voice/make_voice.sh +++ b/tools/voice/make_voice.sh @@ -5,17 +5,18 @@ # tools/voice/make_voice.sh # just the lines that changed # tools/voice/make_voice.sh --all # every line again # -# The first run sets up a Python 3.12 environment for Kokoro in -# tools/voice/.venv (about 1 GB, with PyTorch; needs uv) and downloads the -# Kokoro model (about 330 MB) from Hugging Face. +# Needs an Apple-silicon Mac (MLX), ffmpeg and uv. The first run sets up a +# Python 3.12 environment in tools/voice/.venv-qwen and downloads the models +# from Hugging Face: Qwen3-TTS 1.7B Base (about 4 GB) and Whisper large-v3 +# turbo (about 1.6 GB), to check what each take says. cd "$(dirname "$0")/../.." || exit 1 -V=tools/voice/.venv +V=tools/voice/.venv-qwen command -v ffmpeg >/dev/null || { echo "needs ffmpeg (brew install ffmpeg)"; exit 1; } if [ ! -x "$V/bin/python" ]; then command -v uv >/dev/null || { echo "needs uv (https://docs.astral.sh/uv/)"; exit 1; } uv venv -q --python 3.12 "$V" || exit 1 - uv pip install -q --python "$V/bin/python" "kokoro==0.9.4" "misaki[en]==0.9.4" "soundfile==0.14.0" \ - "en_core_web_sm @ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl" || exit 1 + uv pip install -q --python "$V/bin/python" "mlx-audio==0.6.4" "mlx==0.32.4" "mlx-whisper==0.4.3" \ + "soundfile==0.13.1" "numpy==2.5.4" "transformers==5.22.2" || exit 1 fi exec "$V/bin/python" tools/voice/make_voice.py "$@" diff --git a/tools/voice/old_tam/reference.wav b/tools/voice/old_tam/reference.wav new file mode 100644 index 0000000..9c0b1ba Binary files /dev/null and b/tools/voice/old_tam/reference.wav differ diff --git a/tools/voice/old_tam/voice.json b/tools/voice/old_tam/voice.json new file mode 100644 index 0000000..c74d100 --- /dev/null +++ b/tools/voice/old_tam/voice.json @@ -0,0 +1,7 @@ +{ + "_doc": "Old Tam's voice (tools/voice/make_voice.py). reference.wav was made once by Qwen3-TTS VoiceDesign (mlx-community/Qwen3-TTS-12Hz-1.7B-VoiceDesign-bf16) from `description`, saying `reference_text`; Paul picked it (Nov 2026). Every line is then spoken by the Base model cloning that recording, so it's the same voice throughout. Change any of this and every line is re-made.", + "description": "An elderly, toothless-sounding old man with a thin, reedy, quavering voice and a broad West Country English accent, like an eccentric old farmer. Chuckling, rambling and full of mischief, a little out of breath.", + "model": "mlx-community/Qwen3-TTS-12Hz-1.7B-Base-bf16", + "reference_audio": "reference.wav", + "reference_text": "Ha! Awake at last! Washed up on my island, eh? Well, stretch your legs: W, A, S and D to walk, the mouse to look. Shift if you're in a hurry." +}