Old Tam rewritten and re-voiced (Gitea #3): a new script in his own voice, an old West Country farmer (Qwen3-TTS cloning one recording, so he's the same man in every line), each line's best take chosen by checking its words with Whisper
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -22,5 +22,6 @@ Thumbs.db
|
|||||||
# Cloudflare wrangler cache (tools/site deploys)
|
# Cloudflare wrangler cache (tools/site deploys)
|
||||||
.wrangler/
|
.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/
|
||||||
|
tools/voice/.venv-qwen/
|
||||||
|
|||||||
@@ -55,11 +55,14 @@ nednednerb (https://freesound.org/people/nednednerb/sounds/38250/) and
|
|||||||
(https://freesound.org/people/Dave%20Welsh/sounds/194364/). Thank you.
|
(https://freesound.org/people/Dave%20Welsh/sounds/194364/). Thank you.
|
||||||
|
|
||||||
## Old Tam's voice
|
## Old Tam's voice
|
||||||
`voice/` is spoken by **Kokoro** (Kokoro-82M by hexgrad, Apache-2.0,
|
`voice/` is spoken by **Qwen3-TTS** (Qwen3-TTS-12Hz-1.7B by Alibaba's Qwen
|
||||||
https://huggingface.co/hexgrad/Kokoro-82M) with its British "George" voice,
|
team, Apache-2.0, https://huggingface.co/Qwen; run through mlx-audio, MIT).
|
||||||
made a little older (deeper, slower, warmer) by `tools/voice/make_voice.py`.
|
His voice, a reedy old West Country farmer, was designed once from a written
|
||||||
The recordings are generated from our own text; rebuild them with
|
description by Qwen3-TTS VoiceDesign (`tools/voice/old_tam/`), and every line
|
||||||
`tools/voice/make_voice.sh` after changing any 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)
|
## Cinderwild's own (CC0)
|
||||||
`music/`, `ambience/` and `sfx/synth/` are synthesised from scratch by
|
`music/`, `ambience/` and `sfx/synth/` are synthesised from scratch by
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,44 +1,44 @@
|
|||||||
{
|
{
|
||||||
"hint_cold": "91c5088e453fb0e6",
|
"hint_cold": "ac4139441b7a347a",
|
||||||
"hint_cove": "f7e5c0283b31e1cc",
|
"hint_cove": "91eb0dcbc24afe5c",
|
||||||
"hint_crab": "283542496ecd180c",
|
"hint_crab": "3a88cd0772a7676d",
|
||||||
"hint_died": "e865f8a63622b3c3",
|
"hint_died": "7b60d478f6fdbcef",
|
||||||
"hint_diet": "3178b319c44e9fa0",
|
"hint_diet": "b5e9600c2da38bdf",
|
||||||
"hint_freezing": "48b1e505119a0fc9",
|
"hint_freezing": "0a63a53be413d808",
|
||||||
"hint_hard_winter": "5513aca1655bc278",
|
"hint_hard_winter": "03e1316a475c3c43",
|
||||||
"hint_howl": "05792dda3f3f0b60",
|
"hint_howl": "05792dda3f3f0b60",
|
||||||
"hint_hungry": "ad76f22cb8e034a0",
|
"hint_hungry": "c99c9fbc83506c43",
|
||||||
"hint_night": "2a07d9c0bfae4bf9",
|
"hint_night": "04fc83670a1fd9e9",
|
||||||
"hint_rotten": "98b69de03dbee089",
|
"hint_rotten": "ecb8aecde8c6c762",
|
||||||
"hint_seam": "b353f46816d329b7",
|
"hint_seam": "05531946c57cb97d",
|
||||||
"hint_sheltered": "ba6d6ff8bc4a1c80",
|
"hint_sheltered": "7d3ebd27c230bc79",
|
||||||
"hint_sick": "ec07963c46638ffc",
|
"hint_sick": "b039c2449304ac53",
|
||||||
"hint_snow": "8f0ac7d093f73840",
|
"hint_snow": "90a2e7f6735e4c83",
|
||||||
"hint_stamina": "82bf5b903db7a29f",
|
"hint_stamina": "e3e6cad6202abcaa",
|
||||||
"hint_storm": "f06bb82d15d991c8",
|
"hint_storm": "477b250b6a9c97cb",
|
||||||
"hint_wet": "6aef86b85d67d7ac",
|
"hint_wet": "ebf9b034487bf92e",
|
||||||
"hint_wolf_moon": "723b3dbd5439980c",
|
"hint_wolf_moon": "d66b43dc8a23b5ed",
|
||||||
"tutorial_animals": "e82eb7e35e866812",
|
"tutorial_animals": "20b2eabd8de50d58",
|
||||||
"tutorial_axe": "4bb51d6ea8b42840",
|
"tutorial_axe": "c9a632910c8eaf19",
|
||||||
"tutorial_bed": "34cf4c842db48c19",
|
"tutorial_bed": "46f0130e86504f5b",
|
||||||
"tutorial_berries": "a7cd3805f11c8898",
|
"tutorial_berries": "31d4fd1560fcf46f",
|
||||||
"tutorial_campfire": "0a26bd897d1d8a90",
|
"tutorial_campfire": "6a7790be69235e80",
|
||||||
"tutorial_cook": "7164a8ab27a7821d",
|
"tutorial_cook": "180b4f1413608105",
|
||||||
"tutorial_diet": "fd47408ab4f2b5de",
|
"tutorial_diet": "d44066db5adc0f78",
|
||||||
"tutorial_finish": "36c07fc5a64c9d65",
|
"tutorial_finish": "a9960edb587c3176",
|
||||||
"tutorial_fish": "32f2f3228c036136",
|
"tutorial_fish": "7945d260ceff912b",
|
||||||
"tutorial_flax": "a87ff75ed8864ca6",
|
"tutorial_flax": "7e4f439fc7079272",
|
||||||
"tutorial_hut": "a10bfa289939dfb2",
|
"tutorial_hut": "57c364fcde4bbff9",
|
||||||
"tutorial_look": "27acdf33905cc1a3",
|
"tutorial_look": "4998d8b95ff4fa8e",
|
||||||
"tutorial_mushroom": "79edcfb1afe18c8c",
|
"tutorial_mushroom": "ae582e11c9214891",
|
||||||
"tutorial_pickaxe": "177e0474e4d0fda6",
|
"tutorial_pickaxe": "0697d3610e8a70c5",
|
||||||
"tutorial_rabbit": "6d429ab2cbdc7b19",
|
"tutorial_rabbit": "62544c47f9813cc4",
|
||||||
"tutorial_rod": "d610281e722e8fd3",
|
"tutorial_rod": "0aeae228c3e1e2ed",
|
||||||
"tutorial_sleep": "e202cbf133500c2a",
|
"tutorial_sleep": "51fc176c04af2683",
|
||||||
"tutorial_sticks": "e526e30ddf8756a7",
|
"tutorial_sticks": "5a350695e62a28ef",
|
||||||
"tutorial_torch": "68808fca608352fc",
|
"tutorial_torch": "4bdb8837ac441e6e",
|
||||||
"tutorial_tree": "1dfb2ab0491d962f",
|
"tutorial_tree": "efcbe43e71059ba1",
|
||||||
"tutorial_weapon": "a17d1fe6d5067b8d",
|
"tutorial_weapon": "4d04fcd046eae1e2",
|
||||||
"tutorial_wolf": "feb6bdda64e28161",
|
"tutorial_wolf": "b0725af812cc8d90",
|
||||||
"tutorial_workbench": "1b6108ab18f05de9"
|
"tutorial_workbench": "dd05c80fb8b3c3b8"
|
||||||
}
|
}
|
||||||
@@ -1,44 +1,45 @@
|
|||||||
{
|
{
|
||||||
"hint_cold": "91c5088e453fb0e6",
|
"_voice": "c55ffaa0e19b744b",
|
||||||
"hint_cove": "f7e5c0283b31e1cc",
|
"hint_cold": "ac4139441b7a347a",
|
||||||
"hint_crab": "283542496ecd180c",
|
"hint_cove": "91eb0dcbc24afe5c",
|
||||||
"hint_died": "e865f8a63622b3c3",
|
"hint_crab": "3a88cd0772a7676d",
|
||||||
"hint_diet": "3178b319c44e9fa0",
|
"hint_died": "7b60d478f6fdbcef",
|
||||||
"hint_freezing": "48b1e505119a0fc9",
|
"hint_diet": "b5e9600c2da38bdf",
|
||||||
"hint_hard_winter": "5513aca1655bc278",
|
"hint_freezing": "0a63a53be413d808",
|
||||||
|
"hint_hard_winter": "03e1316a475c3c43",
|
||||||
"hint_howl": "05792dda3f3f0b60",
|
"hint_howl": "05792dda3f3f0b60",
|
||||||
"hint_hungry": "ad76f22cb8e034a0",
|
"hint_hungry": "c99c9fbc83506c43",
|
||||||
"hint_night": "2a07d9c0bfae4bf9",
|
"hint_night": "04fc83670a1fd9e9",
|
||||||
"hint_rotten": "98b69de03dbee089",
|
"hint_rotten": "ecb8aecde8c6c762",
|
||||||
"hint_seam": "b353f46816d329b7",
|
"hint_seam": "05531946c57cb97d",
|
||||||
"hint_sheltered": "ba6d6ff8bc4a1c80",
|
"hint_sheltered": "7d3ebd27c230bc79",
|
||||||
"hint_sick": "ec07963c46638ffc",
|
"hint_sick": "b039c2449304ac53",
|
||||||
"hint_snow": "8f0ac7d093f73840",
|
"hint_snow": "90a2e7f6735e4c83",
|
||||||
"hint_stamina": "82bf5b903db7a29f",
|
"hint_stamina": "e3e6cad6202abcaa",
|
||||||
"hint_storm": "f06bb82d15d991c8",
|
"hint_storm": "477b250b6a9c97cb",
|
||||||
"hint_wet": "6aef86b85d67d7ac",
|
"hint_wet": "ebf9b034487bf92e",
|
||||||
"hint_wolf_moon": "723b3dbd5439980c",
|
"hint_wolf_moon": "d66b43dc8a23b5ed",
|
||||||
"tutorial_animals": "e82eb7e35e866812",
|
"tutorial_animals": "20b2eabd8de50d58",
|
||||||
"tutorial_axe": "4bb51d6ea8b42840",
|
"tutorial_axe": "c9a632910c8eaf19",
|
||||||
"tutorial_bed": "34cf4c842db48c19",
|
"tutorial_bed": "46f0130e86504f5b",
|
||||||
"tutorial_berries": "a7cd3805f11c8898",
|
"tutorial_berries": "31d4fd1560fcf46f",
|
||||||
"tutorial_campfire": "b21eb5ddefb03f1a",
|
"tutorial_campfire": "6a7790be69235e80",
|
||||||
"tutorial_cook": "7164a8ab27a7821d",
|
"tutorial_cook": "180b4f1413608105",
|
||||||
"tutorial_diet": "fd47408ab4f2b5de",
|
"tutorial_diet": "d44066db5adc0f78",
|
||||||
"tutorial_finish": "36c07fc5a64c9d65",
|
"tutorial_finish": "a9960edb587c3176",
|
||||||
"tutorial_fish": "32f2f3228c036136",
|
"tutorial_fish": "7945d260ceff912b",
|
||||||
"tutorial_flax": "a87ff75ed8864ca6",
|
"tutorial_flax": "7e4f439fc7079272",
|
||||||
"tutorial_hut": "a10bfa289939dfb2",
|
"tutorial_hut": "57c364fcde4bbff9",
|
||||||
"tutorial_look": "a88cca377c368962",
|
"tutorial_look": "2345ea0b154d4014",
|
||||||
"tutorial_mushroom": "b641b54d2ef35404",
|
"tutorial_mushroom": "ae582e11c9214891",
|
||||||
"tutorial_pickaxe": "177e0474e4d0fda6",
|
"tutorial_pickaxe": "0697d3610e8a70c5",
|
||||||
"tutorial_rabbit": "6d429ab2cbdc7b19",
|
"tutorial_rabbit": "62544c47f9813cc4",
|
||||||
"tutorial_rod": "d610281e722e8fd3",
|
"tutorial_rod": "0aeae228c3e1e2ed",
|
||||||
"tutorial_sleep": "e202cbf133500c2a",
|
"tutorial_sleep": "51fc176c04af2683",
|
||||||
"tutorial_sticks": "e526e30ddf8756a7",
|
"tutorial_sticks": "5a350695e62a28ef",
|
||||||
"tutorial_torch": "06ebc4c122fd8902",
|
"tutorial_torch": "4bdb8837ac441e6e",
|
||||||
"tutorial_tree": "58c2488e73c1fe4c",
|
"tutorial_tree": "efcbe43e71059ba1",
|
||||||
"tutorial_weapon": "a17d1fe6d5067b8d",
|
"tutorial_weapon": "4d04fcd046eae1e2",
|
||||||
"tutorial_wolf": "feb6bdda64e28161",
|
"tutorial_wolf": "b0725af812cc8d90",
|
||||||
"tutorial_workbench": "1b6108ab18f05de9"
|
"tutorial_workbench": "dd05c80fb8b3c3b8"
|
||||||
}
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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).",
|
"_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": [
|
"hints": [
|
||||||
{"id": "died", "when": "event:died", "title": "Back on your feet",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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."},
|
"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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",
|
{"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."}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
"id": "look",
|
"id": "look",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Look around",
|
"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": {
|
"goal": {
|
||||||
"walk": 10
|
"walk": 10
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"id": "sticks",
|
"id": "sticks",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Gather sticks and stones",
|
"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": {
|
"marker": {
|
||||||
"nearest": "pickup:stick|stone"
|
"nearest": "pickup:stick|stone"
|
||||||
},
|
},
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"id": "flax",
|
"id": "flax",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Pick wild flax",
|
"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": {
|
"marker": {
|
||||||
"nearest": "pickup:fiber"
|
"nearest": "pickup:fiber"
|
||||||
},
|
},
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
"id": "axe",
|
"id": "axe",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Craft a stone axe",
|
"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": {
|
"marker": {
|
||||||
"nearest": "pickup:flint",
|
"nearest": "pickup:flint",
|
||||||
"unless_have": {
|
"unless_have": {
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
"id": "tree",
|
"id": "tree",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Chop down a tree",
|
"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": {
|
"marker": {
|
||||||
"nearest": "tree"
|
"nearest": "tree"
|
||||||
},
|
},
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
"id": "pickaxe",
|
"id": "pickaxe",
|
||||||
"chapter": "tools",
|
"chapter": "tools",
|
||||||
"title": "Craft a pickaxe and break a boulder",
|
"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": {
|
"marker": {
|
||||||
"nearest": "rock"
|
"nearest": "rock"
|
||||||
},
|
},
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
"id": "campfire",
|
"id": "campfire",
|
||||||
"chapter": "food",
|
"chapter": "food",
|
||||||
"title": "Build a campfire",
|
"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": {
|
"goal": {
|
||||||
"placed": "campfire"
|
"placed": "campfire"
|
||||||
}
|
}
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
"id": "berries",
|
"id": "berries",
|
||||||
"chapter": "food",
|
"chapter": "food",
|
||||||
"title": "Pick berries and eat some",
|
"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": {
|
"goal": {
|
||||||
"all": [
|
"all": [
|
||||||
{
|
{
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
"id": "mushroom",
|
"id": "mushroom",
|
||||||
"chapter": "food",
|
"chapter": "food",
|
||||||
"title": "Roast a mushroom",
|
"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": {
|
"goal": {
|
||||||
"all": [
|
"all": [
|
||||||
{
|
{
|
||||||
@@ -169,7 +169,7 @@
|
|||||||
"id": "diet",
|
"id": "diet",
|
||||||
"chapter": "food",
|
"chapter": "food",
|
||||||
"title": "Mind your diet",
|
"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": {
|
"goal": {
|
||||||
"continue": true
|
"continue": true
|
||||||
}
|
}
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
"id": "workbench",
|
"id": "workbench",
|
||||||
"chapter": "hunt",
|
"chapter": "hunt",
|
||||||
"title": "Build a workbench",
|
"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": {
|
"goal": {
|
||||||
"placed": "workbench"
|
"placed": "workbench"
|
||||||
}
|
}
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
"id": "weapon",
|
"id": "weapon",
|
||||||
"chapter": "hunt",
|
"chapter": "hunt",
|
||||||
"title": "Make a weapon",
|
"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": {
|
"goal": {
|
||||||
"any": [
|
"any": [
|
||||||
{
|
{
|
||||||
@@ -203,7 +203,7 @@
|
|||||||
"id": "rabbit",
|
"id": "rabbit",
|
||||||
"chapter": "hunt",
|
"chapter": "hunt",
|
||||||
"title": "Hunt a rabbit",
|
"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": {
|
"goal": {
|
||||||
"killed": "rabbit"
|
"killed": "rabbit"
|
||||||
},
|
},
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
"id": "cook",
|
"id": "cook",
|
||||||
"chapter": "hunt",
|
"chapter": "hunt",
|
||||||
"title": "Butcher it and cook the meat",
|
"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": {
|
"goal": {
|
||||||
"all": [
|
"all": [
|
||||||
{
|
{
|
||||||
@@ -241,7 +241,7 @@
|
|||||||
"id": "animals",
|
"id": "animals",
|
||||||
"chapter": "hunt",
|
"chapter": "hunt",
|
||||||
"title": "Know your animals",
|
"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": {
|
"goal": {
|
||||||
"continue": true
|
"continue": true
|
||||||
}
|
}
|
||||||
@@ -250,7 +250,7 @@
|
|||||||
"id": "rod",
|
"id": "rod",
|
||||||
"chapter": "fish",
|
"chapter": "fish",
|
||||||
"title": "Make a fishing rod",
|
"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": {
|
"goal": {
|
||||||
"crafted": "fishing_rod"
|
"crafted": "fishing_rod"
|
||||||
}
|
}
|
||||||
@@ -259,7 +259,7 @@
|
|||||||
"id": "fish",
|
"id": "fish",
|
||||||
"chapter": "fish",
|
"chapter": "fish",
|
||||||
"title": "Catch a fish and grill it",
|
"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": {
|
"goal": {
|
||||||
"all": [
|
"all": [
|
||||||
{
|
{
|
||||||
@@ -285,7 +285,7 @@
|
|||||||
"id": "hut",
|
"id": "hut",
|
||||||
"chapter": "night",
|
"chapter": "night",
|
||||||
"title": "Build a little hut",
|
"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": {
|
"goal": {
|
||||||
"shelter": true
|
"shelter": true
|
||||||
},
|
},
|
||||||
@@ -300,7 +300,7 @@
|
|||||||
"id": "bed",
|
"id": "bed",
|
||||||
"chapter": "night",
|
"chapter": "night",
|
||||||
"title": "Make a bed and sleep spot",
|
"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": {
|
"goal": {
|
||||||
"all": [
|
"all": [
|
||||||
{
|
{
|
||||||
@@ -316,7 +316,7 @@
|
|||||||
"id": "torch",
|
"id": "torch",
|
||||||
"chapter": "night",
|
"chapter": "night",
|
||||||
"title": "Night is coming: make a torch",
|
"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": {
|
"goal": {
|
||||||
"crafted": "torch"
|
"crafted": "torch"
|
||||||
},
|
},
|
||||||
@@ -328,7 +328,7 @@
|
|||||||
"id": "wolf",
|
"id": "wolf",
|
||||||
"chapter": "night",
|
"chapter": "night",
|
||||||
"title": "Wolves",
|
"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": {
|
"goal": {
|
||||||
"demo_wolf": true
|
"demo_wolf": true
|
||||||
},
|
},
|
||||||
@@ -340,7 +340,7 @@
|
|||||||
"id": "sleep",
|
"id": "sleep",
|
||||||
"chapter": "night",
|
"chapter": "night",
|
||||||
"title": "Sleep till morning",
|
"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": {
|
"goal": {
|
||||||
"slept": true
|
"slept": true
|
||||||
},
|
},
|
||||||
@@ -351,6 +351,6 @@
|
|||||||
],
|
],
|
||||||
"finish": {
|
"finish": {
|
||||||
"title": "You'll do",
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ phases M2.0-M2.7).
|
|||||||
- Audio, music, weather. *Done (0.2.0-0.2.1):* mood music (calm, night,
|
- 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
|
storm, danger, boss) that crossfades with what's happening, ambience (CC0
|
||||||
birdsong, crickets, owls, fire), sound effects for every action (Kenney
|
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/`
|
Sources and licences: `assets/audio/CREDITS.md`; tools in `tools/audio/`
|
||||||
and `tools/voice/`.
|
and `tools/voice/`.
|
||||||
- Help: the player guide on the download site (`tools/site/`), written
|
- Help: the player guide on the download site (`tools/site/`), written
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func _body() -> void:
|
|||||||
sv.wet = 60.0
|
sv.wet = 60.0
|
||||||
await _for(3.0)
|
await _for(3.0)
|
||||||
check(got == ["wet"], "soaked: the wet tip")
|
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.wet = 0.0
|
||||||
|
|
||||||
sv.hunger = 10.0
|
sv.hunger = 10.0
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
"""Old Tam's voice: every line he says (the tutorial's steps and goodbye in
|
"""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,
|
data/tutorial.json, his tips in data/hints.json), in his own voice: Qwen3-TTS
|
||||||
Apache-2.0, hexgrad) with its British "George" voice, then made a little older:
|
(Alibaba's Qwen team, Apache-2.0) cloning tools/voice/old_tam/reference.wav,
|
||||||
deeper, slower and warmer. Output: assets/audio/voice/<line id>.ogg plus
|
a reedy old West Country farmer that Qwen3's VoiceDesign made from a written
|
||||||
lines.json, the text each file was made from (the game only plays a file
|
description (old_tam/voice.json). Cloning that one recording, rather than
|
||||||
whose text still matches, so an edited line goes quiet rather than saying
|
describing him again for each line, keeps him the same man throughout.
|
||||||
the old words; tests/test_tutorial.gd checks they're all up to date).
|
|
||||||
|
|
||||||
Run it with tools/voice/make_voice.sh (sets up the Kokoro environment once).
|
Each line is recorded TAKES times; Whisper (mlx-whisper) writes down what
|
||||||
Only lines whose text changed are re-made; --all re-makes everything.
|
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/<line id>.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__), "..", ".."))
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
OUT = os.path.join(ROOT, "assets", "audio", "voice")
|
OUT = os.path.join(ROOT, "assets", "audio", "voice")
|
||||||
VOICE = "bm_george"
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
SPEED = 0.9
|
VOICE = json.load(open(os.path.join(HERE, "old_tam", "voice.json")))
|
||||||
# Older: down about 1.4 semitones (and so a touch slower), then a little
|
MODEL = VOICE["model"]
|
||||||
# warmth and the top taken off.
|
REF_AUDIO = os.path.join(HERE, "old_tam", VOICE["reference_audio"])
|
||||||
AGE = "asetrate=24000*0.92,aresample=24000,atempo=1.04,lowpass=f=6500,bass=g=2:f=180"
|
REF_TEXT = VOICE["reference_text"]
|
||||||
|
WHISPER = "mlx-community/whisper-large-v3-turbo"
|
||||||
|
TAKES = 3
|
||||||
|
|
||||||
|
|
||||||
def lines():
|
def lines():
|
||||||
@@ -45,19 +55,24 @@ def spoken(text):
|
|||||||
s = s.replace(" · ", ", ").replace("&", "and")
|
s = s.replace(" · ", ", ").replace("&", "and")
|
||||||
s = re.sub(r"\bLMB\b", "left click", s)
|
s = re.sub(r"\bLMB\b", "left click", s)
|
||||||
s = re.sub(r"\bRMB\b", "right 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)
|
s = re.sub(r"\s+,", ",", s)
|
||||||
return s
|
return s
|
||||||
|
|
||||||
|
|
||||||
def _changed_by_new_rules(text):
|
def _words(s):
|
||||||
"""Whether a line has something the spoken() rules added since the last
|
return re.sub(r"[^a-z' ]", " ", s.lower().replace("-", " ")).split()
|
||||||
full recording (0.6.2): "WASD"."""
|
|
||||||
return re.search(r"\bWASD\b", text) is not None
|
|
||||||
|
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():
|
def main():
|
||||||
redo_all = "--all" in sys.argv
|
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)
|
os.makedirs(OUT, exist_ok=True)
|
||||||
manifest_path = os.path.join(OUT, "lines.json")
|
manifest_path = os.path.join(OUT, "lines.json")
|
||||||
old = json.load(open(manifest_path)) if os.path.exists(manifest_path) else {}
|
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.)
|
# key: that's what the game checks against the screen.)
|
||||||
said_path = os.path.join(OUT, "spoken.json")
|
said_path = os.path.join(OUT, "spoken.json")
|
||||||
said = json.load(open(said_path)) if os.path.exists(said_path) else {}
|
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()
|
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"))]
|
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()}
|
keep = {i for i, _ in lines()}
|
||||||
for f in os.listdir(OUT): # lines that no longer exist
|
for f in os.listdir(OUT): # lines that no longer exist
|
||||||
if f.endswith(".ogg") and f[:-4] not in keep:
|
if f.endswith(".ogg") and f[:-4] not in keep:
|
||||||
os.remove(os.path.join(OUT, f))
|
os.remove(os.path.join(OUT, f))
|
||||||
|
report = []
|
||||||
if todo:
|
if todo:
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import soundfile as sf
|
import soundfile as sf
|
||||||
from kokoro import KPipeline
|
import mlx_whisper
|
||||||
pipe = KPipeline(lang_code="b", repo_id="hexgrad/Kokoro-82M")
|
from mlx_audio.tts.utils import load_model
|
||||||
for i, text in todo:
|
model = load_model(MODEL)
|
||||||
audio = np.concatenate([a for _, _, a in pipe(spoken(text), voice=VOICE, speed=SPEED)])
|
sr = getattr(model, "sample_rate", 24000)
|
||||||
tmp = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
|
tmpdir = tempfile.mkdtemp()
|
||||||
sf.write(tmp, audio, 24000)
|
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")
|
path = os.path.join(OUT, i + ".ogg")
|
||||||
# (ffmpeg's own Vorbis encoder only writes stereo)
|
# (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",
|
subprocess.run(["ffmpeg", "-v", "error", "-y", "-i", wav, "-af", "apad=pad_dur=0.25", "-ac", "2",
|
||||||
"-c:a", "vorbis", "-strict", "-2", "-q:a", "4", path], check=True)
|
"-c:a", "vorbis", "-strict", "-2", "-q:a", "5", path], check=True)
|
||||||
os.remove(tmp)
|
|
||||||
old[i] = key(text)
|
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()}
|
manifest = {i: key(t) for i, t in lines()}
|
||||||
json.dump(manifest, open(manifest_path, "w"), indent="\t", sort_keys=True)
|
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)))
|
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__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -5,17 +5,18 @@
|
|||||||
# tools/voice/make_voice.sh # just the lines that changed
|
# tools/voice/make_voice.sh # just the lines that changed
|
||||||
# tools/voice/make_voice.sh --all # every line again
|
# tools/voice/make_voice.sh --all # every line again
|
||||||
#
|
#
|
||||||
# The first run sets up a Python 3.12 environment for Kokoro in
|
# Needs an Apple-silicon Mac (MLX), ffmpeg and uv. The first run sets up a
|
||||||
# tools/voice/.venv (about 1 GB, with PyTorch; needs uv) and downloads the
|
# Python 3.12 environment in tools/voice/.venv-qwen and downloads the models
|
||||||
# Kokoro model (about 330 MB) from Hugging Face.
|
# 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
|
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; }
|
command -v ffmpeg >/dev/null || { echo "needs ffmpeg (brew install ffmpeg)"; exit 1; }
|
||||||
if [ ! -x "$V/bin/python" ]; then
|
if [ ! -x "$V/bin/python" ]; then
|
||||||
command -v uv >/dev/null || { echo "needs uv (https://docs.astral.sh/uv/)"; exit 1; }
|
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 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" \
|
uv pip install -q --python "$V/bin/python" "mlx-audio==0.6.4" "mlx==0.32.4" "mlx-whisper==0.4.3" \
|
||||||
"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
|
"soundfile==0.13.1" "numpy==2.5.4" "transformers==5.22.2" || exit 1
|
||||||
fi
|
fi
|
||||||
exec "$V/bin/python" tools/voice/make_voice.py "$@"
|
exec "$V/bin/python" tools/voice/make_voice.py "$@"
|
||||||
|
|||||||
BIN
tools/voice/old_tam/reference.wav
Normal file
BIN
tools/voice/old_tam/reference.wav
Normal file
Binary file not shown.
7
tools/voice/old_tam/voice.json
Normal file
7
tools/voice/old_tam/voice.json
Normal file
@@ -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."
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user