Spaces:
Running
Running
| /** | |
| * Predefined taxonomy for JS Reachy Mini apps. | |
| * | |
| * These slugs are the ONLY valid output values for the LLM | |
| * inference step (anything else is dropped at parse time) and | |
| * the values consumers (mobile shell, website) filter on. | |
| * | |
| * Why a closed list instead of free-form tags | |
| * โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| * The HF Spaces catalog has no usable categorization for the | |
| * reachy_mini_js_app subset (only platform/SDK tags). We bridge | |
| * the gap by inferring categories with an LLM, but we have to | |
| * constrain the model's output: a closed list keeps category | |
| * pages stable, lets us pre-pick emojis/labels, and avoids the | |
| * "30 near-duplicate slugs" problem you'd get with free-form. | |
| * | |
| * Bumping the taxonomy | |
| * โโโโโโโโโโโโโโโโโโโโ | |
| * Adding, removing or renaming a slug changes the meaning of | |
| * cached entries. Bump TAXONOMY_VERSION when you do that: the | |
| * cache layer compares each entry's `taxonomyVersion` against | |
| * the live one and recomputes stale ones on the next pass. | |
| */ | |
| /** | |
| * Bump this when the slug list OR the descriptions change in a way | |
| * that affects the LLM output. The cache layer invalidates entries | |
| * whose taxonomyVersion is older than this and reclassifies them on | |
| * the next pass. We don't bump it for cosmetic edits (label / emoji) | |
| * since those don't reach the LLM. | |
| * | |
| * History: | |
| * - v1: initial 8-slug taxonomy. | |
| * - v2: added `games`, tightened `kids` + `dev-tools` descriptions, | |
| * switched the prompt to a DECISION ALGORITHM with few-shot. | |
| * - v3: switched from multi-label (up to 3 slugs) to single-label | |
| * (exactly 1 slug). Each app surfaces in exactly one category | |
| * section on the mobile shell - no duplicates across swipers. | |
| * - v4: renamed `dance` to `motion` (broader: marionette, replay, | |
| * choreography without music). Music-driven dance parties | |
| * now belong to `music` since music is what drives them. | |
| */ | |
| export const TAXONOMY_VERSION = 4; | |
| /** | |
| * Canonical category list. Keep slugs short, kebab-case, and | |
| * memorable: they end up in URLs (e.g. `?cat=music`) and in | |
| * filter chips on mobile. | |
| * | |
| * The `description` field is the SOLE source of truth the LLM | |
| * sees - keep them factual, scope-bounded, and example-led so | |
| * the model has signal for both inclusion and exclusion. | |
| */ | |
| export const CATEGORIES = [ | |
| { | |
| slug: 'music', | |
| label: 'Music & Beats', | |
| emoji: '๐ต', | |
| description: | |
| 'Music creation, playback, beats, songs, DJ mixing, instruments, ' + | |
| 'blind-test music games, AND music-driven dance parties (Reachy ' + | |
| 'dances to a song). Requires actual music (rhythm / melody / song). ' + | |
| 'Arbitrary audio (Morse code, alarms, TTS, sound effects) is NOT ' + | |
| 'music. Pure choreography without music belongs to `motion`.', | |
| }, | |
| { | |
| slug: 'motion', | |
| label: 'Motion & Movement', | |
| emoji: '๐ฆพ', | |
| description: | |
| "Apps that drive Reachy's physical movement on its own: motion " + | |
| 'replay, marionette-style remote control of the body, kinetic ' + | |
| 'shows, choreographies WITHOUT music, expressive body language. ' + | |
| 'If the movement is synced to music, use `music` instead.', | |
| }, | |
| { | |
| slug: 'voice', | |
| label: 'Voice & Conversation', | |
| emoji: '๐ฃ๏ธ', | |
| description: | |
| 'Reachy talks, listens, or holds a real-time voice ' + | |
| 'conversation: TTS players, LLM-driven chat (OpenAI Realtime, ' + | |
| 'Claude, Perplexity), wake-word demos, daily reports / news / ' + | |
| 'weather read aloud.', | |
| }, | |
| { | |
| slug: 'storytelling', | |
| label: 'Stories', | |
| emoji: '๐', | |
| description: | |
| 'Narrative stories WITH plot and characters: interactive ' + | |
| 'fiction, bedtime tales, audio adventures, choose-your-own-' + | |
| 'adventure. NOT for daily reports, news, weather, or Q&A ' + | |
| '(those are `voice`).', | |
| }, | |
| { | |
| slug: 'kids', | |
| label: 'For Kids', | |
| emoji: '๐ง', | |
| description: | |
| 'Apps that EXPLICITLY target children: the words kids / ' + | |
| "children / 'for curious minds' / bedtime / 'learning for kids' " + | |
| 'must appear in the name or description, OR the app must be ' + | |
| 'obviously kid-targeted. Combines with `storytelling`, `voice`, ' + | |
| 'or `games`. Lifestyle, sports, weather, generic personality / ' + | |
| 'narration / fun framings are NOT kids.', | |
| }, | |
| { | |
| slug: 'games', | |
| label: 'Games & Play', | |
| emoji: '๐ฎ', | |
| description: | |
| 'Apps with a play loop: scores, rounds, win/lose conditions, ' + | |
| 'quizzes, puzzles, sports simulations, dice/oracles (magic ' + | |
| '8-ball), arcade-style mini-games.', | |
| }, | |
| { | |
| slug: 'vision', | |
| label: 'Vision & Camera', | |
| emoji: '๐๏ธ', | |
| description: | |
| "Apps where Reachy's camera DRIVES behaviour: face/hand/pose " + | |
| 'tracking, image classification, gesture detection, visual ' + | |
| 'mimicry. Merely streaming or displaying the camera feed ' + | |
| '(WebRTC demos, remote-control viewers) is NOT vision.', | |
| }, | |
| { | |
| slug: 'companion', | |
| label: 'Companion', | |
| emoji: '๐ค', | |
| description: | |
| 'Apps with an EXPLICIT emotional / personality / buddy framing ' + | |
| 'in the name or description (companion, buddy, friend, mood, ' + | |
| 'emotional, personality, pet, Tamagotchi-like, "alive", ' + | |
| '"life companion"). Being friendly is not enough.', | |
| }, | |
| { | |
| slug: 'dev-tools', | |
| label: 'Dev & Demos', | |
| emoji: '๐ ๏ธ', | |
| description: | |
| 'RESERVED slug - see DECISION ALGORITHM step 1 in the prompt. ' + | |
| 'Use ONLY for pure technical artefacts (debug utilities, SDK ' + | |
| 'probes, minimal protocol demos, dev-only test spaces) with no ' + | |
| 'end-user experience. When used, it is the SOLE category - ' + | |
| 'never combined with another slug.', | |
| }, | |
| ]; | |
| export const ALLOWED_SLUGS = new Set(CATEGORIES.map((c) => c.slug)); | |
| export function isValidSlug(slug) { | |
| return ALLOWED_SLUGS.has(slug); | |
| } | |
| /** | |
| * Public projection of the taxonomy meant to be shipped to clients | |
| * (mobile shell, website filter chips). We strip the `description` | |
| * field on purpose: it is sized + worded for the LLM prompt and | |
| * carries no UI value (clients render `label` + `emoji`). Render | |
| * order is the index in `CATEGORIES`, surfaced as `order` so a | |
| * client that needs to re-sort (e.g. alphabetical view) keeps the | |
| * canonical order one field-away. | |
| * | |
| * The shape is intentionally minimal and stable: | |
| * `{ slug, label, emoji, order }`. Adding optional fields later | |
| * (e.g. `color`, `shortLabel`) is forward-compatible; renaming or | |
| * dropping one is a breaking change for any client mirror. | |
| */ | |
| export function getPublicTaxonomy() { | |
| return CATEGORIES.map((c, index) => ({ | |
| slug: c.slug, | |
| label: c.label, | |
| emoji: c.emoji, | |
| order: index, | |
| })); | |
| } | |
| /** | |
| * Render the taxonomy as a bulleted list for the LLM prompt. | |
| * Format mirrors what the model is asked to output (slug first) | |
| * to nudge it towards copying the exact string back. | |
| */ | |
| export function buildLlmCategoryList() { | |
| return CATEGORIES.map((c) => `- ${c.slug}: ${c.description}`).join('\n'); | |
| } | |
| /** | |
| * Sanitize a raw LLM-returned list of slugs: | |
| * - drop non-strings | |
| * - lowercase + trim | |
| * - drop unknown slugs (hallucinations) | |
| * - dedupe while preserving order (the model orders by relevance) | |
| * - cap to MAX_CATEGORIES | |
| * | |
| * Returns a fresh array; never mutates input. | |
| */ | |
| export function sanitizeSlugs(raw, maxCategories = 3) { | |
| if (!Array.isArray(raw)) return []; | |
| const seen = new Set(); | |
| const out = []; | |
| for (const v of raw) { | |
| if (typeof v !== 'string') continue; | |
| const slug = v.trim().toLowerCase(); | |
| if (!slug || seen.has(slug)) continue; | |
| if (!ALLOWED_SLUGS.has(slug)) continue; | |
| seen.add(slug); | |
| out.push(slug); | |
| if (out.length >= maxCategories) break; | |
| } | |
| return out; | |
| } | |