feat(launchpad): surface Stories + Voice Gallery; fix gallery key-spread warning (#228)

- Launchpad showed only Clone/Design/Dub. Add ActionCards for the two newer
  modes: Stories (multi-voice audiobooks → setMode('stories')) and Voice Gallery
  (browse designed-voice archetypes → setMode('gallery')). i18n in en + zh-CN,
  backfilled across all 21 locales.
- VoiceGallery: stop spreading a 'key' prop into <ArchetypeCard {...cardProps}>
  (React dev warning + ignored). cardProps no longer carries key; pass key={a.id}
  directly at the two render sites.

Verified: tsc clean, build OK, vitest green, CJK guard passes.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Palash Debnath
2026-06-01 13:56:47 +05:30
committed by GitHub
co-authored by Claude Opus 4.8
parent 94ed7e6347
commit c2ce92cf69
23 changed files with 155 additions and 43 deletions
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "أكمل الدبلجة أولاً — ثم ثبّت التحديث.",
"whats_new": "ما هو الجديد",
"failed": "فشل التحديث",
"retry": "أعد المحاولة"
"retry": "أعد المحاولة",
"dismiss": "استبعاد"
},
"nav": {
"clone": "استنساخ",
@@ -183,7 +184,11 @@
"locked": "مغلق",
"open": "مفتوح",
"try_it": "جربه",
"audio_only": "الصوت فقط"
"audio_only": "الصوت فقط",
"stories_title": "قصص",
"stories_desc": "كتب صوتية متعددة الأصوات - ألقي شخصياتك، وأدخل النص، وقم بالتصدير حسب الفصل.",
"gallery_title": "معرض الصوت",
"gallery_desc": "تصفح الأصوات المصممة الجاهزة حسب اللهجة والعمر والأسلوب - بدون إعداد."
},
"clone": {
"prompt": "موجه",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Beende zuerst deine Synchronisation dann installiere das Update.",
"whats_new": "Was ist neu?",
"failed": "Update fehlgeschlagen",
"retry": "Versuchen Sie es noch einmal"
"retry": "Versuchen Sie es noch einmal",
"dismiss": "Entlassen"
},
"nav": {
"clone": "Klonen",
@@ -184,7 +185,11 @@
"locked": "GESPERRT",
"open": "Offen",
"try_it": "Probieren Sie es aus",
"audio_only": "Nur Audio"
"audio_only": "Nur Audio",
"stories_title": "Geschichten",
"stories_desc": "Mehrsprachige Hörbücher Besetzung Ihrer Charaktere, Einfügen eines Skripts, Export nach Kapitel.",
"gallery_title": "Sprachgalerie",
"gallery_desc": "Durchsuchen Sie vorgefertigte Stimmen nach Akzent, Alter und Stil ohne Einrichtung."
},
"clone": {
"prompt": "Prompt",
+4
View File
@@ -121,6 +121,10 @@
"design_desc": "Build a new voice from a sentence. Gender, age, accent, mood — your call.",
"dub_title": "Video Dubbing",
"dub_desc": "Transcribe, translate, re-voice. Keep each speaker, line up the timing, ship it.",
"stories_title": "Stories",
"stories_desc": "Multi-voice audiobooks — cast your characters, drop in a script, export by chapter.",
"gallery_title": "Voice Gallery",
"gallery_desc": "Browse ready-made designed voices by accent, age and style — no setup.",
"ab_compare": "A/B Compare",
"ab_compare_title": "Try two voices side by side",
"cloned_voices": "Cloned Voices",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Termina tu doblaje primero — luego instala la actualización.",
"whats_new": "¿Qué hay de nuevo?",
"failed": "La actualización falló",
"retry": "Reintentar"
"retry": "Reintentar",
"dismiss": "Descartar"
},
"nav": {
"clone": "Clonar",
@@ -184,7 +185,11 @@
"locked": "BLOQUEADO",
"open": "Abierto",
"try_it": "Pruébalo",
"audio_only": "Sólo audio"
"audio_only": "Sólo audio",
"stories_title": "Historias",
"stories_desc": "Audiolibros con varias voces: elige tus personajes, introduce un guión y exporta por capítulo.",
"gallery_title": "Galería de voz",
"gallery_desc": "Explore voces diseñadas ya preparadas por acento, edad y estilo, sin configuración."
},
"clone": {
"prompt": "rápido",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Terminez d'abord votre doublage, puis installez la mise à jour.",
"whats_new": "Quoi de neuf",
"failed": "La mise à jour a échoué",
"retry": "Réessayer"
"retry": "Réessayer",
"dismiss": "Rejeter"
},
"nav": {
"clone": "Cloner",
@@ -184,7 +185,11 @@
"locked": "VERROUILLÉ",
"open": "Ouvert",
"try_it": "Essayez-le",
"audio_only": "Audio uniquement"
"audio_only": "Audio uniquement",
"stories_title": "Histoires",
"stories_desc": "Livres audio multi-voix : lancez vos personnages, insérez un script, exportez par chapitre.",
"gallery_title": "Galerie vocale",
"gallery_desc": "Parcourez les voix prêtes à lemploi par accent, âge et style aucune configuration."
},
"clone": {
"prompt": "Invite",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "पहले अपनी डबिंग पूरी करें — फिर अपडेट इंस्टॉल करें।",
"whats_new": "नया क्या है",
"failed": "अद्यतन विफल रहा",
"retry": "पुनः प्रयास करें"
"retry": "पुनः प्रयास करें",
"dismiss": "ख़ारिज करें"
},
"nav": {
"clone": "क्लोन",
@@ -183,7 +184,11 @@
"locked": "ताला लगा दिया",
"open": "खुला",
"try_it": "इसे आज़माएं",
"audio_only": "केवल ऑडियो"
"audio_only": "केवल ऑडियो",
"stories_title": "कहानियां",
"stories_desc": "मल्टी-वॉइस ऑडियोबुक - अपने पात्रों को कास्ट करें, एक स्क्रिप्ट डालें, अध्याय के अनुसार निर्यात करें।",
"gallery_title": "आवाज गैलरी",
"gallery_desc": "उच्चारण, उम्र और शैली के आधार पर तैयार डिज़ाइन वाली आवाज़ें ब्राउज़ करें - कोई सेटअप नहीं।"
},
"clone": {
"prompt": "शीघ्र",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Selesaikan sulih suara Anda dulu — lalu instal pembaruannya.",
"whats_new": "Apa yang baru",
"failed": "Pembaruan gagal",
"retry": "Coba lagi"
"retry": "Coba lagi",
"dismiss": "Singkirkan"
},
"nav": {
"clone": "Kloning",
@@ -183,7 +184,11 @@
"locked": "TERKUNCI",
"open": "Buka",
"try_it": "Cobalah",
"audio_only": "Hanya Audio"
"audio_only": "Hanya Audio",
"stories_title": "Cerita",
"stories_desc": "Buku audio multi-suara — masukkan karakter Anda, masukkan skrip, ekspor per bab.",
"gallery_title": "Galeri Suara",
"gallery_desc": "Telusuri suara rancangan siap pakai berdasarkan aksen, usia, dan gaya — tanpa penyiapan."
},
"clone": {
"prompt": "Cepat",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Completa prima il tuo doppiaggio, poi installa l'aggiornamento.",
"whats_new": "Cosa c'è di nuovo",
"failed": "Aggiornamento non riuscito",
"retry": "Riprova"
"retry": "Riprova",
"dismiss": "Ignora"
},
"nav": {
"clone": "Clona",
@@ -183,7 +184,11 @@
"locked": "BLOCCATO",
"open": "Aperto",
"try_it": "Provalo",
"audio_only": "Solo audio"
"audio_only": "Solo audio",
"stories_title": "Storie",
"stories_desc": "Audiolibri a più voci: lancia i tuoi personaggi, inserisci una sceneggiatura, esporta per capitolo.",
"gallery_title": "Galleria vocale",
"gallery_desc": "Sfoglia le voci già pronte per accento, età e stile, senza alcuna configurazione."
},
"clone": {
"prompt": "Richiedi",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "先に吹き替えを完了してから、アップデートをインストールしてください。",
"whats_new": "新機能",
"failed": "アップデートに失敗しました",
"retry": "再試行"
"retry": "再試行",
"dismiss": "解雇する"
},
"nav": {
"clone": "複製",
@@ -184,7 +185,11 @@
"locked": "ロックされています",
"open": "開く",
"try_it": "試してみてください",
"audio_only": "音声のみ"
"audio_only": "音声のみ",
"stories_title": "ストーリー",
"stories_desc": "マルチボイスオーディオブック — キャラクターをキャストし、スクリプトを入力し、章ごとにエクスポートします。",
"gallery_title": "ボイスギャラリー",
"gallery_desc": "アクセント、年齢、スタイルごとに既製のデザインされた音声を参照できます。セットアップは必要ありません。"
},
"clone": {
"prompt": "プロンプト",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "먼저 더빙을 완료한 후 업데이트를 설치하세요.",
"whats_new": "새로운 소식",
"failed": "업데이트 실패",
"retry": "재시도"
"retry": "재시도",
"dismiss": "닫기"
},
"nav": {
"clone": "복제",
@@ -183,7 +184,11 @@
"locked": "잠김",
"open": "열기",
"try_it": "사용해 보세요",
"audio_only": "오디오만"
"audio_only": "오디오만",
"stories_title": "스토리",
"stories_desc": "다중 음성 오디오북 — 캐릭터 캐스팅, 스크립트 삽입, 장별로 내보내기.",
"gallery_title": "보이스 갤러리",
"gallery_desc": "악센트, 연령, 스타일별로 미리 디자인된 음성을 찾아보세요. 설정이 필요 없습니다."
},
"clone": {
"prompt": "프롬프트",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Voltooi eerst je nasynchronisatie — installeer daarna de update.",
"whats_new": "Wat is er nieuw",
"failed": "Update mislukt",
"retry": "Opnieuw proberen"
"retry": "Opnieuw proberen",
"dismiss": "Negeren"
},
"nav": {
"clone": "Klonen",
@@ -183,7 +184,11 @@
"locked": "GESLOTEN",
"open": "Openen",
"try_it": "Probeer het",
"audio_only": "Alleen audio"
"audio_only": "Alleen audio",
"stories_title": "Verhalen",
"stories_desc": "Audioboeken met meerdere stemmen: cast je personages, voeg een script toe en exporteer per hoofdstuk.",
"gallery_title": "Stemgalerij",
"gallery_desc": "Blader door kant-en-klare ontworpen stemmen op accent, leeftijd en stijl - geen installatie."
},
"clone": {
"prompt": "Prompt",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Najpierw dokończ dubbing — potem zainstaluj aktualizację.",
"whats_new": "Co nowego?",
"failed": "Aktualizacja nie powiodła się",
"retry": "Spróbuj ponownie"
"retry": "Spróbuj ponownie",
"dismiss": "Odrzuć"
},
"nav": {
"clone": "Klonuj",
@@ -183,7 +184,11 @@
"locked": "ZABLOKOWANE",
"open": "Otwórz",
"try_it": "Spróbuj",
"audio_only": "Tylko dźwięk"
"audio_only": "Tylko dźwięk",
"stories_title": "Historie",
"stories_desc": "Audiobooki z wieloma głosami — rzucaj postaciami, wstawiaj scenariusz, eksportuj według rozdziałów.",
"gallery_title": "Galeria Głosów",
"gallery_desc": "Przeglądaj gotowe, zaprojektowane głosy według akcentu, wieku i stylu — bez konieczności konfiguracji."
},
"clone": {
"prompt": "Podpowiedź",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Termine sua dublagem primeiro — depois instale a atualização.",
"whats_new": "O que há de novo",
"failed": "Falha na atualização",
"retry": "Tentar novamente"
"retry": "Tentar novamente",
"dismiss": "Dispensar"
},
"nav": {
"clone": "Clonar",
@@ -183,7 +184,11 @@
"locked": "BLOQUEADO",
"open": "Abrir",
"try_it": "Experimente",
"audio_only": "Somente áudio"
"audio_only": "Somente áudio",
"stories_title": "Histórias",
"stories_desc": "Audiolivros com múltiplas vozes escolha seus personagens, insira um roteiro, exporte por capítulo.",
"gallery_title": "Galeria de Voz",
"gallery_desc": "Procure vozes projetadas e prontas por sotaque, idade e estilo — sem configuração."
},
"clone": {
"prompt": "Alerta",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Сначала завершите дубляж — затем установите обновление.",
"whats_new": "Что нового",
"failed": "Обновление не удалось",
"retry": "Повторить попытку"
"retry": "Повторить попытку",
"dismiss": "Увольнять"
},
"nav": {
"clone": "Клонировать",
@@ -183,7 +184,11 @@
"locked": "ЗАБЛОКИРОВАНО",
"open": "Открыть",
"try_it": "Попробуйте это",
"audio_only": "Только аудио"
"audio_only": "Только аудио",
"stories_title": "Истории",
"stories_desc": "Многоголосые аудиокниги — выбирайте своих персонажей, добавляйте сценарий, экспортируйте по главам.",
"gallery_title": "Голосовая галерея",
"gallery_desc": "Просматривайте готовые голоса по акценту, возрасту и стилю — без настройки."
},
"clone": {
"prompt": "Быстрый",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Slutför din dubbning först installera sedan uppdateringen.",
"whats_new": "Vad är nytt",
"failed": "Uppdateringen misslyckades",
"retry": "Försök igen"
"retry": "Försök igen",
"dismiss": "Avvisa"
},
"nav": {
"clone": "Klona",
@@ -183,7 +184,11 @@
"locked": "LÅST",
"open": "Öppna",
"try_it": "Prova det",
"audio_only": "Endast ljud"
"audio_only": "Endast ljud",
"stories_title": "Berättelser",
"stories_desc": "Ljudböcker med flera röster cast dina karaktärer, släpp in ett manus, exportera efter kapitel.",
"gallery_title": "Röstgalleri",
"gallery_desc": "Bläddra bland färdigdesignade röster efter accent, ålder och stil inga inställningar."
},
"clone": {
"prompt": "Fråga",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "ทำการพากย์เสียงของคุณให้เสร็จก่อน แล้วจึงติดตั้งอัปเดต",
"whats_new": "มีอะไรใหม่",
"failed": "การอัปเดตล้มเหลว",
"retry": "ลองอีกครั้ง"
"retry": "ลองอีกครั้ง",
"dismiss": "ยกเลิก"
},
"nav": {
"clone": "โคลนเสียง",
@@ -183,7 +184,11 @@
"locked": "ล็อคแล้ว",
"open": "เปิด",
"try_it": "ลองมัน",
"audio_only": "เสียงเท่านั้น"
"audio_only": "เสียงเท่านั้น",
"stories_title": "เรื่องราว",
"stories_desc": "หนังสือเสียงหลายเสียง — เลือกตัวละครของคุณ ใส่สคริปต์ ส่งออกตามบท",
"gallery_title": "แกลเลอรี่เสียง",
"gallery_desc": "เรียกดูเสียงที่ออกแบบสำเร็จรูปตามสำเนียง อายุ และสไตล์ — ไม่ต้องตั้งค่าใดๆ"
},
"clone": {
"prompt": "พรอมต์",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Önce dublajını bitir — sonra güncellemeyi yükle.",
"whats_new": "Yenilikler",
"failed": "Güncelleme başarısız oldu",
"retry": "Yeniden dene"
"retry": "Yeniden dene",
"dismiss": "Reddet"
},
"nav": {
"clone": "Klonla",
@@ -183,7 +184,11 @@
"locked": "KİLİTLİ",
"open": "Açık",
"try_it": "Deneyin",
"audio_only": "Yalnızca Ses"
"audio_only": "Yalnızca Ses",
"stories_title": "Hikayeler",
"stories_desc": "Çok sesli sesli kitaplar — karakterlerinizi yayınlayın, bir komut dosyası ekleyin, bölüme göre dışa aktarın.",
"gallery_title": "Ses Galerisi",
"gallery_desc": "Hazır tasarlanmış seslere aksan, yaş ve stile göre göz atın; kurulum gerektirmez."
},
"clone": {
"prompt": "İstemi",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Спочатку завершіть дубляж — потім встановіть оновлення.",
"whats_new": "Що нового",
"failed": "Не вдалося оновити",
"retry": "Повторіть спробу"
"retry": "Повторіть спробу",
"dismiss": "Відхилити"
},
"nav": {
"clone": "Клонувати",
@@ -183,7 +184,11 @@
"locked": "ЗАБЛОКУВАНО",
"open": "відкритий",
"try_it": "Спробуйте",
"audio_only": "Лише аудіо"
"audio_only": "Лише аудіо",
"stories_title": "оповідання",
"stories_desc": "Багатоголосові аудіокниги — вибирайте персонажів, вставляйте сценарій, експортуйте за розділами.",
"gallery_title": "Голосова галерея",
"gallery_desc": "Переглядайте готові розроблені голоси за акцентом, віком і стилем — без налаштування."
},
"clone": {
"prompt": "Підкажіть",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "Hoàn tất lồng tiếng của bạn trước — rồi cài đặt bản cập nhật.",
"whats_new": "Có gì mới",
"failed": "Cập nhật không thành công",
"retry": "Thử lại"
"retry": "Thử lại",
"dismiss": "Loại bỏ"
},
"nav": {
"clone": "Nhân bản",
@@ -183,7 +184,11 @@
"locked": "ĐÃ KHÓA",
"open": "Mở",
"try_it": "Hãy thử nó",
"audio_only": "Chỉ âm thanh"
"audio_only": "Chỉ âm thanh",
"stories_title": "Truyện",
"stories_desc": "Sách nói nhiều giọng nói — chọn nhân vật, nhập tập lệnh, xuất theo chương.",
"gallery_title": "Thư viện giọng nói",
"gallery_desc": "Duyệt qua các giọng nói được thiết kế sẵn theo giọng, độ tuổi và phong cách — không cần thiết lập."
},
"clone": {
"prompt": "Lời nhắc",
+6 -1
View File
@@ -8,7 +8,8 @@
"busy": "请先完成配音,再安装更新。",
"whats_new": "更新内容",
"failed": "更新失败",
"retry": "重试"
"retry": "重试",
"dismiss": "解雇"
},
"stories": {
"title": "故事编辑器",
@@ -120,6 +121,10 @@
"design_desc": "用一句话构建新声线。性别、年龄、口音、情绪 — 由你决定。",
"dub_title": "视频配音",
"dub_desc": "转录、翻译、重新配音。保留每位说话人,对齐时间轴,然后导出。",
"stories_title": "故事",
"stories_desc": "多角色有声书——为角色分配语音,粘贴脚本,按章节导出。",
"gallery_title": "语音库",
"gallery_desc": "按口音、年龄和风格浏览现成的设计语音——无需设置。",
"ab_compare": "A/B 对比",
"ab_compare_title": "并排试听两个声音",
"cloned_voices": "克隆语音",
+7 -2
View File
@@ -8,7 +8,8 @@
"busy": "請先完成配音,再安裝更新。",
"whats_new": "有什麼新消息",
"failed": "更新失敗",
"retry": "重試"
"retry": "重試",
"dismiss": "解僱"
},
"nav": {
"clone": "複製聲音",
@@ -183,7 +184,11 @@
"locked": "鎖定",
"open": "打開",
"try_it": "嘗試一下",
"audio_only": "僅音訊"
"audio_only": "僅音訊",
"stories_title": "故事",
"stories_desc": "多語音有聲書 — 塑造角色、插入劇本、依章節導出。",
"gallery_title": "語音畫廊",
"gallery_desc": "按口音、年齡和風格瀏覽現成的設計聲音 - 無需設定。"
},
"clone": {
"prompt": "提示",
+7 -1
View File
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import {
Scale, Fingerprint, Wand2, Film, Lock,
Scale, Fingerprint, Wand2, Film, Lock, BookOpen, LibraryBig,
} from 'lucide-react';
import { API } from '../api/client';
import ReadinessChecklist from '../components/ReadinessChecklist';
@@ -138,6 +138,12 @@ export default function Launchpad({
<ActionCard hue="#fe8019" Icon={Film} title={t('launchpad.dub_title')} accent="🎬" count={studioProjects.length} onClick={() => setMode('dub')}>
{t('launchpad.dub_desc')}
</ActionCard>
<ActionCard hue="#83a598" Icon={BookOpen} title={t('launchpad.stories_title')} accent="📖" onClick={() => setMode('stories')}>
{t('launchpad.stories_desc')}
</ActionCard>
<ActionCard hue="#fabd2f" Icon={LibraryBig} title={t('launchpad.gallery_title')} accent="🎭" onClick={() => setMode('gallery')}>
{t('launchpad.gallery_desc')}
</ActionCard>
</div>
{/* Demo profile callout */}
+5 -3
View File
@@ -242,8 +242,10 @@ function ArchetypesZone({
const favSet = useMemo(() => new Set(favorites), [favorites]);
const applyFav = (list) => (favOnly ? list.filter((a) => favSet.has(a.id)) : list);
// NOTE: no `key` here — React keys must be passed directly on the element,
// not spread in (spreading a `key` prop triggers a dev warning + is ignored).
const cardProps = (a) => ({
key: a.id, a, t, viewMode,
a, t, viewMode,
isFavorite: favSet.has(a.id),
isPlaying: playingId === a.id,
isLoadingPreview: loadingPreviewId === a.id,
@@ -308,7 +310,7 @@ function ArchetypesZone({
<section className="archetype-section">
<div className="content-header"><div className="content-title">{t('archetypes.featured', { defaultValue: 'Featured' })}</div></div>
<div className={`archetype-grid ${viewMode}`}>
{applyFav(featured).map((a) => <ArchetypeCard {...cardProps(a)} />)}
{applyFav(featured).map((a) => <ArchetypeCard key={a.id} {...cardProps(a)} />)}
</div>
</section>
)}
@@ -325,7 +327,7 @@ function ArchetypesZone({
) : (
<>
<div className={`archetype-grid ${viewMode}`}>
{applyFav(browse).map((a) => <ArchetypeCard {...cardProps(a)} />)}
{applyFav(browse).map((a) => <ArchetypeCard key={a.id} {...cardProps(a)} />)}
</div>
{applyFav(browse).length === 0 && (
<div className="empty">{t('gallery.no_matches', { defaultValue: 'No voices match these filters.' })}</div>