diff --git a/apps/web/public/locales/de/translation.json b/apps/web/public/locales/de/translation.json index 5c70fdf..08f09cc 100644 --- a/apps/web/public/locales/de/translation.json +++ b/apps/web/public/locales/de/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Schriftgröße", "k1e68fe8a": "Cutia Onboarding", "k1e8e8c34": "Spezialisiert auf Tempo, Übergänge und Zeitlinienstruktur", + "k1f07c90": "Vorlagenentwurf konnte nicht erstellt werden", "k1f0ee77d": "Vom Gerät", "k1f61d17e": "Textdauer ausrichten", "k1fbd515d": "Klicken Sie hier, um das vollständige Bild anzuzeigen", @@ -91,6 +92,7 @@ "k355373d": "Alle", "k35b37981": "Ripple-Bearbeitung", "k36475a14": "Video-, Audio-, Text-, Sticker-Spuren", + "k371fd2a0": "Das Erstellen eines Vorlagenentwurfs ersetzt die aktuellen Szenenspuren. Sie können den Vorgang anschließend rückgängig machen.", "k3783e3ef": "Modell wird geladen {{progress}}%", "k378f89ea": "Keine Referenzbilder", "k3822fc21": "Gespeichert", @@ -205,6 +207,7 @@ "k6c262505": "Videoerstellung fehlgeschlagen", "k6c3c4937": "Datenschutzorientierte lokale Verarbeitung", "k6c48feef": "Keine Beschreibung", + "k6c4c8ae0": "Importieren Sie zuerst visuelle Assets", "k6c6d5798": "{{num}} Generationen", "k6c7f361c": "Audio wird extrahiert...", "k6c8bdc62": "Stummschalten", @@ -220,6 +223,7 @@ "k70000a20": "Sound speichern", "k7034b515": "Teilen", "k70a6a7ec": "Schriftart", + "k713d4cc5": "Vorlagenentwurf erstellt", "k71c8feb": "Neues Projekt", "k72136300": "KI-Agent für automatisierte Video-Bearbeitung", "k72da871b": "Schatten", @@ -237,6 +241,7 @@ "k790e4102": "Marken", "k7927b58d": "Argumente", "k7969d4e1": "Auto (Direktor)", + "k79cf0712": "Audio-Assets", "k7a9fdb71": "Projekt wird exportiert", "k7aae526f": "Ziehen Sie Videos, Fotos und Audiodateien hierher", "k7aee6f3c": "{{name}}-Übergang auf alle benachbarten Clip-Verbindungsstellen anwenden", @@ -273,6 +278,7 @@ "k87b99feb": "{{success}} generiert, {{fail}} fehlgeschlagen", "k887ae5cd": "Erstellen", "k888cdd97": "Text zu Sprache", + "k88fdb5ec": "Ein-Klick-Vorlagen verwenden die Bilder und Videos, die bereits in diesem Projekt vorhanden sind.", "k89e20696": "Ja. Cutia ist als kostenlose, Open-Source-, datenschutzorientierte Alternative zu CapCut konzipiert. Es bietet KI-native Bearbeitung, Mehrspur-Zeitleiste, MP4/WebM-Export und läuft vollständig in deinem Browser – kein Konto, keine Uploads, keine Wasserzeichen.", "k8abd001a": "Allgemeiner Assistent", "k8affad75": "Webversion verfügbar, Desktop-App bevorzugt", @@ -300,6 +306,7 @@ "k94d21c36": "Verarbeitung...", "k9574787f": "Keine Sounds gefunden", "k9582ed10": "Hat Cutia die gleichen Funktionen wie CapCut?", + "k96ebf9b": "Zu Medien", "k9728a218": "Neuer Charakter", "k97a88cd0": "Desktop-App erfordert Installation", "k97caf1aa": "Denken...", @@ -344,6 +351,7 @@ "ka877374b": "Projekt-ID", "ka96bbd3a": "Spezialisiert auf narrative Struktur, emotionale Entwicklung und kreative Leitung", "kaa748ff5": "Löschen {{type}}", + "kaab263db": "Aktuelle Zeitleiste ersetzen?", "kabed8e08": "Medien", "kac94dcc8": "Dateien auf Server hochgeladen", "kacad5657": "Elemente einfügen", @@ -355,6 +363,7 @@ "kaea3203c": "Projekt wird aktualisiert", "kaf4aa9d7": "MP4 (H.264) - Bessere Kompatibilität", "kafab5b82": "Syntax-Benutzeroberfläche", + "kafb2473b": "Visuelle Assets", "kafd3c560": "Kürzliche Sticker löschen", "kb01f4f95": "Fertig", "kb090ddd": "Preis", diff --git a/apps/web/public/locales/en/translation.json b/apps/web/public/locales/en/translation.json index 9bb2899..e4163b5 100644 --- a/apps/web/public/locales/en/translation.json +++ b/apps/web/public/locales/en/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Font size", "k1e68fe8a": "Cutia Onboarding", "k1e8e8c34": "Specializes in pacing, transitions, and timeline structure", + "k1f07c90": "Failed to generate template draft", "k1f0ee77d": "From Device", "k1f61d17e": "Align text duration", "k1fbd515d": "Click to view full image", @@ -91,6 +92,7 @@ "k355373d": "All", "k35b37981": "Ripple editing", "k36475a14": "Video, audio, text, sticker tracks", + "k371fd2a0": "Generating a template draft will replace the current scene tracks. You can undo it afterwards.", "k3783e3ef": "Loading model {{progress}}%", "k378f89ea": "No reference images", "k3822fc21": "Saved", @@ -205,6 +207,7 @@ "k6c262505": "Video generation failed", "k6c3c4937": "Privacy-first local processing", "k6c48feef": "No description", + "k6c4c8ae0": "Import visual assets first", "k6c6d5798": "{{num}} generations", "k6c7f361c": "Extracting audio...", "k6c8bdc62": "Mute", @@ -220,6 +223,7 @@ "k70000a20": "Save sound", "k7034b515": "Split", "k70a6a7ec": "Font", + "k713d4cc5": "Template draft generated", "k71c8feb": "New project", "k72136300": "AI agent for automated video editing", "k72da871b": "Shadow", @@ -237,6 +241,7 @@ "k790e4102": "Brands", "k7927b58d": "Arguments", "k7969d4e1": "Auto (Director)", + "k79cf0712": "Audio assets", "k7a9fdb71": "Exporting project", "k7aae526f": "Drag and drop videos, photos, and audio files here", "k7aee6f3c": "Apply {{name}} transition to all adjacent clip junctions", @@ -273,6 +278,7 @@ "k87b99feb": "{{success}} generated, {{fail}} failed", "k887ae5cd": "Create", "k888cdd97": "Text to Speech", + "k88fdb5ec": "One-click templates use the images and videos already in this project.", "k89e20696": "Yes. Cutia is designed as a free, open-source, privacy-first alternative to CapCut. It offers AI-native editing, multi-track timeline, MP4/WebM export, and runs entirely in your browser — no account, no uploads, no watermarks.", "k8abd001a": "General Assistant", "k8affad75": "Web version available, desktop app preferred", @@ -300,6 +306,7 @@ "k94d21c36": "Processing...", "k9574787f": "No sounds found", "k9582ed10": "Does Cutia have the same features as CapCut?", + "k96ebf9b": "Go to Media", "k9728a218": "New Character", "k97a88cd0": "Desktop app requires installation", "k97caf1aa": "Thinking...", @@ -344,6 +351,7 @@ "ka877374b": "Project ID", "ka96bbd3a": "Specializes in narrative flow, emotional arc, and creative direction", "kaa748ff5": "Delete {{type}}", + "kaab263db": "Replace current timeline?", "kabed8e08": "Media", "kac94dcc8": "Files uploaded to servers", "kacad5657": "Paste elements", @@ -355,6 +363,7 @@ "kaea3203c": "Updating project", "kaf4aa9d7": "MP4 (H.264) - Better compatibility", "kafab5b82": "Syntax UI", + "kafb2473b": "Visual assets", "kafd3c560": "Clear recent stickers", "kb01f4f95": "Done", "kb090ddd": "Price", diff --git a/apps/web/public/locales/es/translation.json b/apps/web/public/locales/es/translation.json index a1bd2d1..4804fc5 100644 --- a/apps/web/public/locales/es/translation.json +++ b/apps/web/public/locales/es/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Tamaño de fuente", "k1e68fe8a": "Onboarding de Cutia", "k1e8e8c34": "Se especializa en ritmo, transiciones y estructura de la línea de tiempo", + "k1f07c90": "No se pudo generar el borrador de la plantilla", "k1f0ee77d": "Desde el dispositivo", "k1f61d17e": "Alinear la duración del texto", "k1fbd515d": "Haz clic para ver la imagen completa", @@ -91,6 +92,7 @@ "k355373d": "Todo", "k35b37981": "Edición de ondas", "k36475a14": "Video, audio, texto, pistas de stickers", + "k371fd2a0": "Generar un borrador de plantilla reemplazará las pistas de la escena actual. Después podrás deshacerlo.", "k3783e3ef": "Cargando modelo {{progress}}%", "k378f89ea": "Sin imágenes de referencia", "k3822fc21": "Guardado", @@ -205,6 +207,7 @@ "k6c262505": "Error en la generación de video", "k6c3c4937": "Procesamiento local con enfoque en la privacidad", "k6c48feef": "Sin descripción", + "k6c4c8ae0": "Importa primero los recursos visuales", "k6c6d5798": "{{num}} generaciones", "k6c7f361c": "Extrayendo audio...", "k6c8bdc62": "Silenciar", @@ -220,6 +223,7 @@ "k70000a20": "Guardar sonido", "k7034b515": "Dividir", "k70a6a7ec": "Fuente", + "k713d4cc5": "Se generó el borrador de la plantilla", "k71c8feb": "Nuevo proyecto", "k72136300": "Agente de IA para edición de video automatizada", "k72da871b": "Sombra", @@ -237,6 +241,7 @@ "k790e4102": "Marcas", "k7927b58d": "Argumentos", "k7969d4e1": "Automático (Director)", + "k79cf0712": "Recursos de audio", "k7a9fdb71": "Exportando proyecto", "k7aae526f": "Arrastra y suelta videos, fotos y archivos de audio aquí", "k7aee6f3c": "Aplicar la transición {{name}} a todas las uniones de clips adyacentes", @@ -273,6 +278,7 @@ "k87b99feb": "{{success}} generado, {{fail}} fallido", "k887ae5cd": "Crear", "k888cdd97": "Texto a voz", + "k88fdb5ec": "Las plantillas de un clic usan las imágenes y los vídeos que ya están en este proyecto.", "k89e20696": "Sí. Cutia está diseñada como una alternativa gratuita, de código abierto y centrada en la privacidad a CapCut. Ofrece edición nativa de IA, línea de tiempo de múltiples pistas, exportación MP4/WebM y funciona completamente en tu navegador: sin cuenta, sin subidas, sin marcas de agua.", "k8abd001a": "Asistente General", "k8affad75": "Versión web disponible, aplicación de escritorio preferida", @@ -300,6 +306,7 @@ "k94d21c36": "Procesando...", "k9574787f": "No se encontraron sonidos", "k9582ed10": "¿Cutia tiene las mismas características que CapCut?", + "k96ebf9b": "Ir a Medios", "k9728a218": "Nuevo personaje", "k97a88cd0": "La aplicación de escritorio requiere instalación", "k97caf1aa": "Pensando...", @@ -344,6 +351,7 @@ "ka877374b": "ID del proyecto", "ka96bbd3a": "Se especializa en flujo narrativo, arco emocional y dirección creativa", "kaa748ff5": "Eliminar {{type}}", + "kaab263db": "¿Reemplazar la línea de tiempo actual?", "kabed8e08": "Medios", "kac94dcc8": "Archivos subidos a servidores", "kacad5657": "Pegar elementos", @@ -355,6 +363,7 @@ "kaea3203c": "Actualizando proyecto", "kaf4aa9d7": "MP4 (H.264) - Mejor compatibilidad", "kafab5b82": "Interfaz de sintaxis", + "kafb2473b": "Recursos visuales", "kafd3c560": "Limpiar stickers recientes", "kb01f4f95": "Hecho", "kb090ddd": "Precio", diff --git a/apps/web/public/locales/fr/translation.json b/apps/web/public/locales/fr/translation.json index 3cc364e..7c3e5f3 100644 --- a/apps/web/public/locales/fr/translation.json +++ b/apps/web/public/locales/fr/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Taille de police", "k1e68fe8a": "Onboarding Cutia", "k1e8e8c34": "Se spécialise dans le rythme, les transitions et la structure de la timeline", + "k1f07c90": "Impossible de générer le brouillon du modèle", "k1f0ee77d": "Depuis l'appareil", "k1f61d17e": "Aligner la durée du texte", "k1fbd515d": "Cliquez pour voir l'image en pleine taille", @@ -91,6 +92,7 @@ "k355373d": "Tout", "k35b37981": "Édition en ripple", "k36475a14": "Pistes vidéo, audio, texte, autocollants", + "k371fd2a0": "La génération d’un brouillon de modèle remplacera les pistes de la scène actuelle. Vous pourrez ensuite annuler l’opération.", "k3783e3ef": "Chargement du modèle {{progress}}%", "k378f89ea": "Pas d'images de référence", "k3822fc21": "Sauvegardé", @@ -205,6 +207,7 @@ "k6c262505": "Échec de la génération de vidéo", "k6c3c4937": "Traitement local axé sur la confidentialité", "k6c48feef": "Pas de description", + "k6c4c8ae0": "Importez d’abord les ressources visuelles", "k6c6d5798": "{{num}} générations", "k6c7f361c": "Extraction de l'audio...", "k6c8bdc62": "Couper le son", @@ -220,6 +223,7 @@ "k70000a20": "Enregistrer le son", "k7034b515": "Diviser", "k70a6a7ec": "Police", + "k713d4cc5": "Brouillon de modèle généré", "k71c8feb": "Nouveau projet", "k72136300": "Agent IA pour l'édition vidéo automatisée", "k72da871b": "Ombre", @@ -237,6 +241,7 @@ "k790e4102": "Marques", "k7927b58d": "Arguments", "k7969d4e1": "Auto (Directeur)", + "k79cf0712": "Ressources audio", "k7a9fdb71": "Exportation du projet", "k7aae526f": "Faites glisser et déposez des vidéos, des photos et des fichiers audio ici", "k7aee6f3c": "Appliquer la transition {{name}} à toutes les jonctions de clips adjacentes", @@ -273,6 +278,7 @@ "k87b99feb": "{{success}} généré, {{fail}} échoué", "k887ae5cd": "Créer", "k888cdd97": "Texte en parole", + "k88fdb5ec": "Les modèles en un clic utilisent les images et vidéos déjà présentes dans ce projet.", "k89e20696": "Oui. Cutia est conçue comme une alternative gratuite, open-source et axée sur la confidentialité à CapCut. Elle offre un montage natif de l'IA, une chronologie multi-pistes, une exportation MP4/WebM, et fonctionne entièrement dans votre navigateur — pas de compte, pas de téléchargements, pas de filigranes.", "k8abd001a": "Assistant général", "k8affad75": "Version web disponible, application de bureau préférée", @@ -300,6 +306,7 @@ "k94d21c36": "Traitement...", "k9574787f": "Aucun son trouvé", "k9582ed10": "Cutia a-t-elle les mêmes fonctionnalités que CapCut ?", + "k96ebf9b": "Aller aux médias", "k9728a218": "Nouveau personnage", "k97a88cd0": "L'application de bureau nécessite une installation", "k97caf1aa": "Réflexion...", @@ -344,6 +351,7 @@ "ka877374b": "ID de projet", "ka96bbd3a": "Se spécialise dans le flux narratif, l'arc émotionnel et la direction créative", "kaa748ff5": "Supprimer {{type}}", + "kaab263db": "Remplacer la timeline actuelle ?", "kabed8e08": "Média", "kac94dcc8": "Fichiers téléchargés sur des serveurs", "kacad5657": "Coller des éléments", @@ -355,6 +363,7 @@ "kaea3203c": "Mise à jour du projet", "kaf4aa9d7": "MP4 (H.264) - Meilleure compatibilité", "kafab5b82": "Interface de syntaxe", + "kafb2473b": "Ressources visuelles", "kafd3c560": "Effacer les autocollants récents", "kb01f4f95": "Terminé", "kb090ddd": "Prix", diff --git a/apps/web/public/locales/id/translation.json b/apps/web/public/locales/id/translation.json index 6e8c616..68a08a1 100644 --- a/apps/web/public/locales/id/translation.json +++ b/apps/web/public/locales/id/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Ukuran font", "k1e68fe8a": "Onboarding Cutia", "k1e8e8c34": "Spesialis dalam ritme, transisi, dan struktur timeline", + "k1f07c90": "Gagal membuat draf template", "k1f0ee77d": "Dari Perangkat", "k1f61d17e": "Durasi teks sejajar", "k1fbd515d": "Klik untuk melihat gambar penuh", @@ -90,6 +91,7 @@ "k355373d": "Semua", "k35b37981": "Pengeditan ripple", "k36475a14": "Video, audio, teks, trek stiker", + "k371fd2a0": "Membuat draf template akan menggantikan track adegan saat ini. Anda dapat membatalkannya setelahnya.", "k3783e3ef": "Memuat model {{progress}}%", "k378f89ea": "Tidak ada gambar referensi", "k3822fc21": "Disimpan", @@ -202,6 +204,7 @@ "k6c262505": "Generasi video gagal", "k6c3c4937": "Pemrosesan lokal yang mengutamakan privasi", "k6c48feef": "Tidak ada deskripsi", + "k6c4c8ae0": "Impor aset visual terlebih dahulu", "k6c6d5798": "{{num}} generasi", "k6c7f361c": "Mengekstrak audio...", "k6c8bdc62": "Diam", @@ -217,6 +220,7 @@ "k70000a20": "Simpan suara", "k7034b515": "Pecah", "k70a6a7ec": "Font", + "k713d4cc5": "Draf template berhasil dibuat", "k71c8feb": "Proyek baru", "k72136300": "Agen AI untuk pengeditan video otomatis", "k72da871b": "Bayangan", @@ -234,6 +238,7 @@ "k790e4102": "Merek", "k7927b58d": "Argumen", "k7969d4e1": "Auto (Sutradara)", + "k79cf0712": "Aset audio", "k7a9fdb71": "Mengekspor proyek", "k7aae526f": "Seret dan lepas video, foto, dan file audio di sini", "k7aee6f3c": "Terapkan transisi {{name}} ke semua persimpangan klip yang berdekatan", @@ -269,6 +274,7 @@ "k87b99feb": "{{success}} dihasilkan, {{fail}} gagal", "k887ae5cd": "Buat", "k888cdd97": "Teks ke Ucapan", + "k88fdb5ec": "Template sekali klik menggunakan gambar dan video yang sudah ada di proyek ini.", "k89e20696": "Ya. Cutia dirancang sebagai alternatif gratis, sumber terbuka, dan mengutamakan privasi untuk CapCut. Ini menawarkan pengeditan AI-native, garis waktu multi-trek, ekspor MP4/WebM, dan berjalan sepenuhnya di browser Anda — tanpa akun, tanpa unggahan, tanpa watermark.", "k8abd001a": "Asisten Umum", "k8affad75": "Versi web tersedia, aplikasi desktop lebih disukai", @@ -296,6 +302,7 @@ "k94d21c36": "Memproses...", "k9574787f": "Tidak ada suara ditemukan", "k9582ed10": "Apakah Cutia memiliki fitur yang sama dengan CapCut?", + "k96ebf9b": "Ke Media", "k9728a218": "Karakter Baru", "k97a88cd0": "Aplikasi desktop memerlukan instalasi", "k97caf1aa": "Berpikir...", @@ -340,6 +347,7 @@ "ka877374b": "ID Proyek", "ka96bbd3a": "Spesialis dalam alur naratif, busur emosional, dan arahan kreatif", "kaa748ff5": "Hapus {{type}}", + "kaab263db": "Ganti timeline saat ini?", "kabed8e08": "Media", "kac94dcc8": "File diunggah ke server", "kacad5657": "Tempel elemen", @@ -351,6 +359,7 @@ "kaea3203c": "Memperbarui proyek", "kaf4aa9d7": "MP4 (H.264) - Kompatibilitas lebih baik", "kafab5b82": "Antarmuka Sintaksis", + "kafb2473b": "Aset visual", "kafd3c560": "Hapus stiker terbaru", "kb01f4f95": "Selesai", "kb090ddd": "Harga", diff --git a/apps/web/public/locales/it/translation.json b/apps/web/public/locales/it/translation.json index 7f9af2f..0a6400e 100644 --- a/apps/web/public/locales/it/translation.json +++ b/apps/web/public/locales/it/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Dimensione del carattere", "k1e68fe8a": "Onboarding Cutia", "k1e8e8c34": "Si specializza in ritmo, transizioni e struttura della timeline", + "k1f07c90": "Impossibile generare la bozza del modello", "k1f0ee77d": "Da dispositivo", "k1f61d17e": "Allinea la durata del testo", "k1fbd515d": "Clicca per visualizzare l'immagine completa", @@ -91,6 +92,7 @@ "k355373d": "Tutti", "k35b37981": "Modifica a onda", "k36475a14": "Video, audio, testo, tracce adesivi", + "k371fd2a0": "La generazione di una bozza del modello sostituirà le tracce della scena corrente. Potrai annullarla in seguito.", "k3783e3ef": "Caricamento modello {{progress}}%", "k378f89ea": "Nessuna immagine di riferimento", "k3822fc21": "Salvato", @@ -205,6 +207,7 @@ "k6c262505": "Generazione video fallita", "k6c3c4937": "Elaborazione locale con priorità alla privacy", "k6c48feef": "Nessuna descrizione", + "k6c4c8ae0": "Importa prima le risorse visive", "k6c6d5798": "{{num}} generazioni", "k6c7f361c": "Estrazione audio...", "k6c8bdc62": "Disattiva l'audio", @@ -220,6 +223,7 @@ "k70000a20": "Salva suono", "k7034b515": "Dividi", "k70a6a7ec": "Carattere", + "k713d4cc5": "Bozza del modello generata", "k71c8feb": "Nuovo progetto", "k72136300": "Agente AI per editing video automatizzato", "k72da871b": "Ombra", @@ -237,6 +241,7 @@ "k790e4102": "Marchi", "k7927b58d": "Argomenti", "k7969d4e1": "Auto (Direttore)", + "k79cf0712": "Risorse audio", "k7a9fdb71": "Esportazione progetto", "k7aae526f": "Trascina e rilascia video, foto e file audio qui", "k7aee6f3c": "Applica la transizione {{name}} a tutte le giunzioni di clip adiacenti", @@ -273,6 +278,7 @@ "k87b99feb": "{{success}} generato, {{fail}} fallito", "k887ae5cd": "Crea", "k888cdd97": "Testo in voce", + "k88fdb5ec": "I modelli con un clic usano le immagini e i video già presenti in questo progetto.", "k89e20696": "Sì. Cutia è progettata come un'alternativa gratuita, open-source e orientata alla privacy a CapCut. Offre editing nativo dell'AI, timeline multi-traccia, esportazione MP4/WebM e funziona interamente nel tuo browser — nessun account, nessun caricamento, nessuna filigrana.", "k8abd001a": "Assistente generale", "k8affad75": "Versione web disponibile, app desktop preferita", @@ -300,6 +306,7 @@ "k94d21c36": "Elaborazione...", "k9574787f": "Nessun suono trovato", "k9582ed10": "Cutia ha le stesse funzionalità di CapCut?", + "k96ebf9b": "Vai a Media", "k9728a218": "Nuovo Personaggio", "k97a88cd0": "L'app desktop richiede installazione", "k97caf1aa": "Pensando...", @@ -344,6 +351,7 @@ "ka877374b": "ID Progetto", "ka96bbd3a": "Si specializza nel flusso narrativo, arco emotivo e direzione creativa", "kaa748ff5": "Elimina {{type}}", + "kaab263db": "Sostituire la timeline corrente?", "kabed8e08": "Media", "kac94dcc8": "File caricati sui server", "kacad5657": "Incolla elementi", @@ -355,6 +363,7 @@ "kaea3203c": "Aggiornamento progetto", "kaf4aa9d7": "MP4 (H.264) - Migliore compatibilità", "kafab5b82": "Interfaccia Sintassi", + "kafb2473b": "Risorse visive", "kafd3c560": "Cancella adesivi recenti", "kb01f4f95": "Fatto", "kb090ddd": "Prezzo", diff --git a/apps/web/public/locales/ja/translation.json b/apps/web/public/locales/ja/translation.json index 757f5f3..47285c6 100644 --- a/apps/web/public/locales/ja/translation.json +++ b/apps/web/public/locales/ja/translation.json @@ -31,6 +31,7 @@ "k1e357029": "フォントサイズ", "k1e68fe8a": "Cutiaオンボーディング", "k1e8e8c34": "ペーシング、トランジション、タイムライン構造に特化しています", + "k1f07c90": "テンプレート下書きを生成できませんでした", "k1f0ee77d": "デバイスから", "k1f61d17e": "テキストの整列時間", "k1fbd515d": "全画像を表示するにはクリックしてください", @@ -90,6 +91,7 @@ "k355373d": "すべて", "k35b37981": "リップル編集", "k36475a14": "動画、音声、テキスト、ステッカートラック", + "k371fd2a0": "テンプレート下書きを生成すると、現在のシーンのトラックが置き換えられます。あとで元に戻せます。", "k3783e3ef": "モデルを読み込んでいます {{progress}}%", "k378f89ea": "参照画像がありません", "k3822fc21": "保存済み", @@ -202,6 +204,7 @@ "k6c262505": "動画生成に失敗しました", "k6c3c4937": "プライバシー重視のローカル処理", "k6c48feef": "説明がありません", + "k6c4c8ae0": "先にビジュアルアセットをインポートしてください", "k6c6d5798": "{{num}} 回の生成", "k6c7f361c": "オーディオを抽出中...", "k6c8bdc62": "ミュート", @@ -217,6 +220,7 @@ "k70000a20": "サウンドを保存", "k7034b515": "分割", "k70a6a7ec": "フォント", + "k713d4cc5": "テンプレート下書きを生成しました", "k71c8feb": "新しいプロジェクト", "k72136300": "自動ビデオ編集のためのAIエージェント", "k72da871b": "シャドウ", @@ -234,6 +238,7 @@ "k790e4102": "ブランド", "k7927b58d": "引数", "k7969d4e1": "オート(ディレクター)", + "k79cf0712": "音声アセット", "k7a9fdb71": "プロジェクトをエクスポート中", "k7aae526f": "ここに動画、写真、オーディオファイルをドラッグ&ドロップ", "k7aee6f3c": "すべての隣接クリップ接続点に{{name}}トランジションを適用", @@ -269,6 +274,7 @@ "k87b99feb": "{{success}} が生成されました, {{fail}} が失敗しました", "k887ae5cd": "作成", "k888cdd97": "テキストから音声へ", + "k88fdb5ec": "ワンクリックテンプレートは、このプロジェクトにすでにある画像と動画を使用します。", "k89e20696": "はい。CutiaはCapCutの無料でオープンソース、プライバシー重視の代替品として設計されています。AIネイティブ編集、マルチトラックタイムライン、MP4/WebMエクスポートを提供し、完全にブラウザ内で動作します — アカウント不要、アップロード不要、透かしなし。", "k8abd001a": "一般アシスタント", "k8affad75": "ウェブ版あり、デスクトップアプリ推奨", @@ -296,6 +302,7 @@ "k94d21c36": "処理中...", "k9574787f": "音声が見つかりません", "k9582ed10": "CutiaはCapCutと同じ機能を持っていますか?", + "k96ebf9b": "メディアへ移動", "k9728a218": "新しいキャラクター", "k97a88cd0": "デスクトップアプリはインストールが必要", "k97caf1aa": "考え中...", @@ -340,6 +347,7 @@ "ka877374b": "プロジェクトID", "ka96bbd3a": "物語の流れ、感情の弧、クリエイティブディレクションに特化しています", "kaa748ff5": "{{type}}を削除", + "kaab263db": "現在のタイムラインを置き換えますか?", "kabed8e08": "メディア", "kac94dcc8": "ファイルはサーバーにアップロードされます", "kacad5657": "要素を貼り付ける", @@ -351,6 +359,7 @@ "kaea3203c": "プロジェクトを更新中", "kaf4aa9d7": "MP4 (H.264) - より良い互換性", "kafab5b82": "構文UI", + "kafb2473b": "ビジュアルアセット", "kafd3c560": "最近のステッカーをクリア", "kb01f4f95": "完了", "kb090ddd": "価格", diff --git a/apps/web/public/locales/ko/translation.json b/apps/web/public/locales/ko/translation.json index f040302..747ef08 100644 --- a/apps/web/public/locales/ko/translation.json +++ b/apps/web/public/locales/ko/translation.json @@ -31,6 +31,7 @@ "k1e357029": "글꼴 크기", "k1e68fe8a": "Cutia 온보딩", "k1e8e8c34": "템포, 전환 및 타임라인 구조 전문", + "k1f07c90": "템플릿 초안을 생성하지 못했습니다", "k1f0ee77d": "장치에서", "k1f61d17e": "텍스트 정렬 시간", "k1fbd515d": "전체 이미지를 보려면 클릭하세요", @@ -90,6 +91,7 @@ "k355373d": "모두", "k35b37981": "리플 편집", "k36475a14": "비디오, 오디오, 텍스트, 스티커 트랙", + "k371fd2a0": "템플릿 초안을 생성하면 현재 장면의 트랙이 교체됩니다. 이후에 되돌릴 수 있습니다.", "k3783e3ef": "모델 로딩 중 {{progress}}%", "k378f89ea": "참조 이미지 없음", "k3822fc21": "저장됨", @@ -202,6 +204,7 @@ "k6c262505": "비디오 생성 실패", "k6c3c4937": "개인 정보 우선 로컬 처리", "k6c48feef": "설명 없음", + "k6c4c8ae0": "먼저 시각 자산을 가져오세요", "k6c6d5798": "{{num}} 생성", "k6c7f361c": "오디오 추출 중...", "k6c8bdc62": "음소거", @@ -217,6 +220,7 @@ "k70000a20": "사운드 저장", "k7034b515": "분할", "k70a6a7ec": "글꼴", + "k713d4cc5": "템플릿 초안이 생성되었습니다", "k71c8feb": "새 프로젝트", "k72136300": "자동화된 비디오 편집을 위한 AI 에이전트", "k72da871b": "그림자", @@ -234,6 +238,7 @@ "k790e4102": "브랜드", "k7927b58d": "인수", "k7969d4e1": "자동 (디렉터)", + "k79cf0712": "오디오 자산", "k7a9fdb71": "프로젝트 내보내기 중", "k7aae526f": "여기에 비디오, 사진 및 오디오 파일을 드래그 앤 드롭하세요", "k7aee6f3c": "모든 인접한 클립 접합점에 {{name}} 전환 적용", @@ -269,6 +274,7 @@ "k87b99feb": "{{success}} 생성됨, {{fail}} 실패함", "k887ae5cd": "생성", "k888cdd97": "텍스트 음성 변환", + "k88fdb5ec": "원클릭 템플릿은 이 프로젝트에 이미 있는 이미지와 비디오를 사용합니다.", "k89e20696": "네. Cutia는 CapCut에 대한 무료, 오픈 소스, 개인 정보 보호 우선 대안으로 설계되었습니다. AI 네이티브 편집, 다중 트랙 타임라인, MP4/WebM 내보내기를 제공하며 전적으로 귀하의 브라우저에서 실행됩니다 — 계정, 업로드, 워터마크 없음.", "k8abd001a": "일반 보조", "k8affad75": "웹 버전 사용 가능, 데스크탑 앱 선호", @@ -296,6 +302,7 @@ "k94d21c36": "처리 중...", "k9574787f": "소리를 찾을 수 없습니다", "k9582ed10": "Cutia가 CapCut과 동일한 기능을 가지고 있나요?", + "k96ebf9b": "미디어로 이동", "k9728a218": "새 캐릭터", "k97a88cd0": "데스크탑 앱은 설치가 필요합니다", "k97caf1aa": "생각 중...", @@ -340,6 +347,7 @@ "ka877374b": "프로젝트 ID", "ka96bbd3a": "서사 흐름, 감정 곡선 및 창의적 방향 전문", "kaa748ff5": "{{type}} 삭제", + "kaab263db": "현재 타임라인을 교체할까요?", "kabed8e08": "미디어", "kac94dcc8": "서버에 업로드된 파일", "kacad5657": "요소 붙여넣기", @@ -351,6 +359,7 @@ "kaea3203c": "프로젝트 업데이트 중", "kaf4aa9d7": "MP4 (H.264) - 더 나은 호환성", "kafab5b82": "구문 UI", + "kafb2473b": "시각 자산", "kafd3c560": "최근 스티커 지우기", "kb01f4f95": "완료", "kb090ddd": "가격", diff --git a/apps/web/public/locales/pt/translation.json b/apps/web/public/locales/pt/translation.json index bc64ad2..42d84a4 100644 --- a/apps/web/public/locales/pt/translation.json +++ b/apps/web/public/locales/pt/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Tamanho da fonte", "k1e68fe8a": "Onboarding do Cutia", "k1e8e8c34": "Especializa-se em ritmo, transições e estrutura de linha do tempo", + "k1f07c90": "Falha ao gerar o rascunho do modelo", "k1f0ee77d": "Do dispositivo", "k1f61d17e": "Alinhar duração do texto", "k1fbd515d": "Clique para ver a imagem completa", @@ -91,6 +92,7 @@ "k355373d": "Todos", "k35b37981": "Edição em ripple", "k36475a14": "Vídeo, áudio, texto, faixas de adesivos", + "k371fd2a0": "Gerar um rascunho do modelo substituirá as faixas da cena atual. Você pode desfazer depois.", "k3783e3ef": "Carregando modelo {{progress}}%", "k378f89ea": "Sem imagens de referência", "k3822fc21": "Salvo", @@ -205,6 +207,7 @@ "k6c262505": "Falha na geração do vídeo", "k6c3c4937": "Processamento local com foco em privacidade", "k6c48feef": "Sem descrição", + "k6c4c8ae0": "Importe primeiro os recursos visuais", "k6c6d5798": "{{num}} gerações", "k6c7f361c": "Extraindo áudio...", "k6c8bdc62": "Silenciar", @@ -220,6 +223,7 @@ "k70000a20": "Salvar som", "k7034b515": "Dividir", "k70a6a7ec": "Fonte", + "k713d4cc5": "Rascunho do modelo gerado", "k71c8feb": "Novo projeto", "k72136300": "Agente de IA para edição de vídeo automatizada", "k72da871b": "Sombra", @@ -237,6 +241,7 @@ "k790e4102": "Marcas", "k7927b58d": "Argumentos", "k7969d4e1": "Auto (Diretor)", + "k79cf0712": "Recursos de áudio", "k7a9fdb71": "Exportando projeto", "k7aae526f": "Arraste e solte vídeos, fotos e arquivos de áudio aqui", "k7aee6f3c": "Aplicar transição {{name}} a todas as junções de clipes adjacentes", @@ -273,6 +278,7 @@ "k87b99feb": "{{success}} gerado, {{fail}} falhou", "k887ae5cd": "Criar", "k888cdd97": "Texto para Fala", + "k88fdb5ec": "Os modelos de um clique usam as imagens e os vídeos que já estão neste projeto.", "k89e20696": "Sim. O Cutia é projetado como uma alternativa gratuita, de código aberto e focada em privacidade ao CapCut. Oferece edição nativa de IA, linha do tempo multi-faixa, exportação MP4/WebM e funciona inteiramente no seu navegador — sem conta, sem uploads, sem marcas d'água.", "k8abd001a": "Assistente Geral", "k8affad75": "Versão web disponível, aplicativo desktop preferido", @@ -300,6 +306,7 @@ "k94d21c36": "Processando...", "k9574787f": "Nenhum som encontrado", "k9582ed10": "O Cutia tem os mesmos recursos que o CapCut?", + "k96ebf9b": "Ir para Mídia", "k9728a218": "Novo Personagem", "k97a88cd0": "O aplicativo desktop requer instalação", "k97caf1aa": "Pensando...", @@ -344,6 +351,7 @@ "ka877374b": "ID do Projeto", "ka96bbd3a": "Especializa-se em fluxo narrativo, arco emocional e direção criativa", "kaa748ff5": "Excluir {{type}}", + "kaab263db": "Substituir a linha do tempo atual?", "kabed8e08": "Mídia", "kac94dcc8": "Arquivos enviados para servidores", "kacad5657": "Colar elementos", @@ -355,6 +363,7 @@ "kaea3203c": "Atualizando projeto", "kaf4aa9d7": "MP4 (H.264) - Melhor compatibilidade", "kafab5b82": "Interface de Sintaxe", + "kafb2473b": "Recursos visuais", "kafd3c560": "Limpar adesivos recentes", "kb01f4f95": "Concluído", "kb090ddd": "Preço", diff --git a/apps/web/public/locales/ru/translation.json b/apps/web/public/locales/ru/translation.json index cfeeb07..a3a3023 100644 --- a/apps/web/public/locales/ru/translation.json +++ b/apps/web/public/locales/ru/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Размер шрифта", "k1e68fe8a": "Введение в Cutia", "k1e8e8c34": "Специализируется на ритме, переходах и структуре таймлайна", + "k1f07c90": "Не удалось создать черновик шаблона", "k1f0ee77d": "С устройства", "k1f61d17e": "Выравнивание продолжительности текста", "k1fbd515d": "Нажмите, чтобы просмотреть полное изображение", @@ -92,6 +93,7 @@ "k355373d": "Все", "k35b37981": "Редактирование с эффектом ряби", "k36475a14": "Видеотреки, аудиотреки, текстовые треки, наклейки", + "k371fd2a0": "Создание черновика шаблона заменит дорожки текущей сцены. После этого действие можно отменить.", "k3783e3ef": "Загрузка модели {{progress}}%", "k378f89ea": "Нет изображений для справки", "k3822fc21": "Сохранено", @@ -208,6 +210,7 @@ "k6c262505": "Ошибка генерации видео", "k6c3c4937": "Локальная обработка с приоритетом конфиденциальности", "k6c48feef": "Нет описания", + "k6c4c8ae0": "Сначала импортируйте визуальные материалы", "k6c6d5798": "{{num}} генераций", "k6c7f361c": "Извлечение аудио...", "k6c8bdc62": "Выключить звук", @@ -223,6 +226,7 @@ "k70000a20": "Сохранить звук", "k7034b515": "Разделить", "k70a6a7ec": "Шрифт", + "k713d4cc5": "Черновик шаблона создан", "k71c8feb": "Новый проект", "k72136300": "ИИ-агент для автоматизированного редактирования видео", "k72da871b": "Тень", @@ -240,6 +244,7 @@ "k790e4102": "Бренды", "k7927b58d": "Аргументы", "k7969d4e1": "Авто (Режиссер)", + "k79cf0712": "Аудиоматериалы", "k7a9fdb71": "Экспорт проекта", "k7aae526f": "Перетащите видео, фотографии и аудиофайлы сюда", "k7aee6f3c": "Применить переход {{name}} ко всем смежным соединениям клипов", @@ -277,6 +282,7 @@ "k87b99feb": "{{success}} сгенерировано, {{fail}} не удалось", "k887ae5cd": "Создать", "k888cdd97": "Текст в речь", + "k88fdb5ec": "Шаблоны в один клик используют изображения и видео, уже добавленные в этот проект.", "k89e20696": "Да. Cutia разработана как бесплатная, с открытым исходным кодом, ориентированная на конфиденциальность альтернатива CapCut. Она предлагает AI-ориентированное редактирование, многодорожечную временную шкалу, экспорт в MP4/WebM и работает полностью в вашем браузере — без аккаунта, без загрузок, без водяных знаков.", "k8abd001a": "Общий помощник", "k8affad75": "Доступна веб-версия, предпочтительно настольное приложение", @@ -304,6 +310,7 @@ "k94d21c36": "Обработка...", "k9574787f": "Звуков не найдено", "k9582ed10": "Есть ли у Cutia те же функции, что и у CapCut?", + "k96ebf9b": "Перейти в Медиа", "k9728a218": "Новый персонаж", "k97a88cd0": "Настольное приложение требует установки", "k97caf1aa": "Думаю...", @@ -348,6 +355,7 @@ "ka877374b": "ID проекта", "ka96bbd3a": "Специализируется на нарративном потоке, эмоциональной арке и креативном направлении", "kaa748ff5": "Удалить {{type}}", + "kaab263db": "Заменить текущую временную шкалу?", "kabed8e08": "Медиа", "kac94dcc8": "Файлы загружаются на серверы", "kacad5657": "Вставить элементы", @@ -359,6 +367,7 @@ "kaea3203c": "Обновление проекта", "kaf4aa9d7": "MP4 (H.264) - Лучшая совместимость", "kafab5b82": "Синтаксис UI", + "kafb2473b": "Визуальные материалы", "kafd3c560": "Очистить недавние стикеры", "kb01f4f95": "Готово", "kb090ddd": "Цена", diff --git a/apps/web/public/locales/vi/translation.json b/apps/web/public/locales/vi/translation.json index 7757cdf..8ae8ff0 100644 --- a/apps/web/public/locales/vi/translation.json +++ b/apps/web/public/locales/vi/translation.json @@ -31,6 +31,7 @@ "k1e357029": "Kích thước phông chữ", "k1e68fe8a": "Hướng dẫn Cutia", "k1e8e8c34": "Chuyên về nhịp độ, chuyển tiếp và cấu trúc thời gian", + "k1f07c90": "Không thể tạo bản nháp mẫu", "k1f0ee77d": "Từ thiết bị", "k1f61d17e": "Căn chỉnh thời gian văn bản", "k1fbd515d": "Nhấp để xem hình ảnh đầy đủ", @@ -90,6 +91,7 @@ "k355373d": "Tất cả", "k35b37981": "Chỉnh sửa Ripple", "k36475a14": "Video, âm thanh, văn bản, các lớp nhãn dán", + "k371fd2a0": "Việc tạo bản nháp mẫu sẽ thay thế các track của cảnh hiện tại. Bạn có thể hoàn tác sau đó.", "k3783e3ef": "Đang tải mô hình {{progress}}%", "k378f89ea": "Không có hình ảnh tham khảo", "k3822fc21": "Đã lưu", @@ -202,6 +204,7 @@ "k6c262505": "Tạo video không thành công", "k6c3c4937": "Xử lý cục bộ ưu tiên quyền riêng tư", "k6c48feef": "Không có mô tả", + "k6c4c8ae0": "Nhập tài nguyên hình ảnh trước", "k6c6d5798": "{{num}} lần tạo", "k6c7f361c": "Đang trích xuất âm thanh...", "k6c8bdc62": "Tắt tiếng", @@ -217,6 +220,7 @@ "k70000a20": "Lưu âm thanh", "k7034b515": "Chia", "k70a6a7ec": "Phông chữ", + "k713d4cc5": "Đã tạo bản nháp mẫu", "k71c8feb": "Dự án mới", "k72136300": "Tác nhân AI cho chỉnh sửa video tự động", "k72da871b": "Bóng", @@ -234,6 +238,7 @@ "k790e4102": "Thương hiệu", "k7927b58d": "Tham số", "k7969d4e1": "Tự động (Giám đốc)", + "k79cf0712": "Tài nguyên âm thanh", "k7a9fdb71": "Đang xuất dự án", "k7aae526f": "Kéo và thả video, ảnh và tệp âm thanh vào đây", "k7aee6f3c": "Áp dụng chuyển tiếp {{name}} cho tất cả các giao điểm clip liền kề", @@ -269,6 +274,7 @@ "k87b99feb": "{{success}} đã được tạo, {{fail}} đã thất bại", "k887ae5cd": "Tạo", "k888cdd97": "Chuyển văn bản thành giọng nói", + "k88fdb5ec": "Các mẫu một lần nhấp sử dụng hình ảnh và video đã có trong dự án này.", "k89e20696": "Có. Cutia được thiết kế như một lựa chọn miễn phí, mã nguồn mở, ưu tiên quyền riêng tư thay thế cho CapCut. Nó cung cấp chỉnh sửa gốc AI, thời gian biểu đa lớp, xuất MP4/WebM, và chạy hoàn toàn trong trình duyệt của bạn — không cần tài khoản, không cần tải lên, không có watermark.", "k8abd001a": "Trợ lý Chung", "k8affad75": "Phiên bản web có sẵn, ứng dụng máy tính để bàn được ưu tiên", @@ -296,6 +302,7 @@ "k94d21c36": "Đang xử lý...", "k9574787f": "Không tìm thấy âm thanh", "k9582ed10": "Cutia có các tính năng giống như CapCut không?", + "k96ebf9b": "Đi tới Phương tiện", "k9728a218": "Nhân vật mới", "k97a88cd0": "Ứng dụng máy tính để bàn yêu cầu cài đặt", "k97caf1aa": "Đang suy nghĩ...", @@ -340,6 +347,7 @@ "ka877374b": "ID Dự án", "ka96bbd3a": "Chuyên về dòng kể chuyện, cung bậc cảm xúc và định hướng sáng tạo", "kaa748ff5": "Xóa {{type}}", + "kaab263db": "Thay thế dòng thời gian hiện tại?", "kabed8e08": "Phương tiện", "kac94dcc8": "Tệp được tải lên máy chủ", "kacad5657": "Dán phần tử", @@ -351,6 +359,7 @@ "kaea3203c": "Cập nhật dự án", "kaf4aa9d7": "MP4 (H.264) - Tương thích tốt hơn", "kafab5b82": "Giao diện cú pháp", + "kafb2473b": "Tài nguyên hình ảnh", "kafd3c560": "Xóa nhãn dán gần đây", "kb01f4f95": "Hoàn tất", "kb090ddd": "Giá", diff --git a/apps/web/public/locales/zh/translation.json b/apps/web/public/locales/zh/translation.json index a527e4c..1ad95ae 100644 --- a/apps/web/public/locales/zh/translation.json +++ b/apps/web/public/locales/zh/translation.json @@ -31,6 +31,7 @@ "k1e357029": "字体大小", "k1e68fe8a": "Cutia 入门", "k1e8e8c34": "专注于节奏、过渡和时间线结构", + "k1f07c90": "生成模板草稿失败", "k1f0ee77d": "从设备", "k1f61d17e": "对齐文本持续时间", "k1fbd515d": "点击查看完整图像", @@ -90,6 +91,7 @@ "k355373d": "全部", "k35b37981": "波纹编辑", "k36475a14": "视频、音频、文本、贴纸轨道", + "k371fd2a0": "生成模板草稿会替换当前场景的轨道内容,生成后仍可通过撤销恢复。", "k3783e3ef": "加载模型中 {{progress}}%", "k378f89ea": "没有参考图像", "k3822fc21": "已保存", @@ -202,6 +204,7 @@ "k6c262505": "视频生成失败", "k6c3c4937": "隐私优先的本地处理", "k6c48feef": "没有描述", + "k6c4c8ae0": "请先导入视觉素材", "k6c6d5798": "{{num}} 次生成", "k6c7f361c": "提取音频中...", "k6c8bdc62": "静音", @@ -217,6 +220,7 @@ "k70000a20": "保存音效", "k7034b515": "拆分", "k70a6a7ec": "字体", + "k713d4cc5": "模板草稿已生成", "k71c8feb": "新建项目", "k72136300": "用于自动视频编辑的AI代理", "k72da871b": "阴影", @@ -234,6 +238,7 @@ "k790e4102": "品牌", "k7927b58d": "参数", "k7969d4e1": "自动(导演)", + "k79cf0712": "音频素材", "k7a9fdb71": "正在导出项目", "k7aae526f": "将视频、照片和音频文件拖放到这里", "k7aee6f3c": "将 {{name}} 转场应用于所有相邻的剪辑交接处", @@ -269,6 +274,7 @@ "k87b99feb": "{{success}} 生成,{{fail}} 失败", "k887ae5cd": "创建", "k888cdd97": "文本转语音", + "k88fdb5ec": "一键模板会直接使用当前项目里已经导入的图片和视频素材。", "k89e20696": "是的。Cutia 被设计为一个免费的、开源的、隐私优先的 CapCut 替代品。它提供 AI 原生编辑、多轨时间线、MP4/WebM 导出,并完全在您的浏览器中运行——无需账户,无需上传,无需水印。", "k8abd001a": "通用助手", "k8affad75": "提供网页版,推荐使用桌面应用", @@ -296,6 +302,7 @@ "k94d21c36": "处理中...", "k9574787f": "未找到音效", "k9582ed10": "Cutia 是否具有与 CapCut 相同的功能?", + "k96ebf9b": "前往媒体", "k9728a218": "新角色", "k97a88cd0": "桌面应用需要安装", "k97caf1aa": "思考中...", @@ -340,6 +347,7 @@ "ka877374b": "项目 ID", "ka96bbd3a": "专注于叙事流、情感弧和创意指导", "kaa748ff5": "删除 {{type}}", + "kaab263db": "替换当前时间线?", "kabed8e08": "媒体", "kac94dcc8": "文件上传到服务器", "kacad5657": "粘贴元素", @@ -351,6 +359,7 @@ "kaea3203c": "更新项目", "kaf4aa9d7": "MP4 (H.264) - 更好的兼容性", "kafab5b82": "语法 UI", + "kafb2473b": "视觉素材", "kafd3c560": "清除最近贴纸", "kb01f4f95": "完成", "kb090ddd": "价格", diff --git a/apps/web/src/components/editor/panels/assets/views/ai-template-cuts.tsx b/apps/web/src/components/editor/panels/assets/views/ai-template-cuts.tsx new file mode 100644 index 0000000..1037b8e --- /dev/null +++ b/apps/web/src/components/editor/panels/assets/views/ai-template-cuts.tsx @@ -0,0 +1,212 @@ +"use client"; + +import { useTranslation } from "@i18next-toolkit/nextjs-approuter"; +import { useState } from "react"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { useEditor } from "@/hooks/use-editor"; +import { invokeAction } from "@/lib/actions"; +import { TEMPLATE_CUTS, type TemplateCutId } from "@/lib/auto-edit/templates"; +import { useAssetsPanelStore } from "@/stores/assets-panel-store"; +import { + AudioWave02Icon, + Folder03Icon, + SparklesIcon, + Video01Icon, +} from "@hugeicons/core-free-icons"; +import { HugeiconsIcon, type IconSvgElement } from "@hugeicons/react"; + +export function AITemplateCutsView() { + const { t } = useTranslation(); + const editor = useEditor(); + const setActiveTab = useAssetsPanelStore((state) => state.setActiveTab); + const [pendingTemplateId, setPendingTemplateId] = + useState(null); + + const mediaAssets = editor.media.getAssets().filter((asset) => !asset.ephemeral); + const visualAssets = mediaAssets.filter( + (asset) => asset.type === "image" || asset.type === "video", + ); + const audioAssets = mediaAssets.filter((asset) => asset.type === "audio"); + + const hasTimelineContent = editor.timeline + .getTracks() + .some((track) => track.elements.length > 0); + + const handleGenerate = ({ templateId }: { templateId: TemplateCutId }) => { + if (hasTimelineContent) { + setPendingTemplateId(templateId); + return; + } + + invokeAction("generate-template-cut", { templateId }, "mouseclick"); + }; + + const handleConfirmReplace = () => { + if (!pendingTemplateId) { + return; + } + + invokeAction( + "generate-template-cut", + { templateId: pendingTemplateId }, + "mouseclick", + ); + setPendingTemplateId(null); + }; + + if (visualAssets.length === 0) { + return ( +
+
+ +
+
+

{t("Import visual assets first")}

+

+ {t( + "One-click templates use the images and videos already in this project.", + )} +

+
+ +
+ ); + } + + return ( + <> +
+
+ + +
+ +
+ {TEMPLATE_CUTS.map((template) => ( +
+
+
+
+ +

{template.name}

+
+

+ {template.description} +

+
+
+ {template.transitionType} +
+
+ +
+ + +
+ + +
+ ))} +
+
+ + { + if (!open) { + setPendingTemplateId(null); + } + }} + > + + + {t("Replace current timeline?")} + + {t( + "Generating a template draft will replace the current scene tracks. You can undo it afterwards.", + )} + + + + {t("Cancel")} + + {t("Generate")} + + + + + + ); +} + +function TemplateMetric({ + icon, + label, + value, +}: { + icon: IconSvgElement; + label: string; + value: number; +}) { + return ( +
+
+ + {label} +
+
{value}
+
+ ); +} + +function TemplateBadge({ + label, + value, +}: { + label: string; + value: string; +}) { + return ( +
+ {label} + {value} +
+ ); +} diff --git a/apps/web/src/components/editor/panels/assets/views/ai.tsx b/apps/web/src/components/editor/panels/assets/views/ai.tsx index 419c70f..25f70c7 100644 --- a/apps/web/src/components/editor/panels/assets/views/ai.tsx +++ b/apps/web/src/components/editor/panels/assets/views/ai.tsx @@ -48,6 +48,7 @@ import { useCharacterStore } from "@/stores/character-store"; import type { AICharacter } from "@/types/character"; import { UserIcon } from "@hugeicons/core-free-icons"; import { Link, useRouter } from "@/lib/navigation"; +import { AITemplateCutsView } from "./ai-template-cuts"; const ASPECT_RATIOS = [ { value: "auto", label: "Auto" }, @@ -1181,6 +1182,11 @@ export function AIView() { label: t("Generate"), content: , }, + { + value: "template-cuts", + label: t("Presets"), + content: , + }, { value: "ai-characters", label: t("Characters"), diff --git a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx index 78f7ded..9e26369 100644 --- a/apps/web/src/components/editor/panels/timeline/timeline-element.tsx +++ b/apps/web/src/components/editor/panels/timeline/timeline-element.tsx @@ -32,7 +32,11 @@ import type { } from "@/types/timeline"; import type { MediaAsset } from "@/types/assets"; import { mediaSupportsAudio } from "@/lib/media/media-utils"; -import { getActionDefinition, type TAction, invokeAction } from "@/lib/actions"; +import { + getActionDefinition, + type TActionWithOptionalArgs, + invokeAction, +} from "@/lib/actions"; import { useElementSelection } from "@/hooks/timeline/element/use-element-selection"; import Image from "next/image"; import { @@ -58,7 +62,7 @@ import { useTranslation } from "@i18next-toolkit/nextjs-approuter"; import type { ComponentProps } from "react"; import { VideoThumbnailStrip } from "./video-thumbnail-strip"; -function getDisplayShortcut(action: TAction) { +function getDisplayShortcut(action: TActionWithOptionalArgs) { const { defaultShortcuts } = getActionDefinition(action); if (!defaultShortcuts?.length) { return ""; @@ -706,7 +710,7 @@ function ActionMenuItem({ children, ...props }: Omit, "onClick" | "textRight"> & { - action: TAction; + action: TActionWithOptionalArgs; }) { return ( { event.stopPropagation(); diff --git a/apps/web/src/core/managers/timeline-manager.ts b/apps/web/src/core/managers/timeline-manager.ts index 4837249..0c26242 100644 --- a/apps/web/src/core/managers/timeline-manager.ts +++ b/apps/web/src/core/managers/timeline-manager.ts @@ -36,6 +36,7 @@ import { UpdateElementStartTimeCommand, MoveElementCommand, DetachAudioCommand, + GenerateTemplateCutCommand, } from "@/lib/commands/timeline"; import { BatchCommand } from "@/lib/commands"; import type { InsertElementParams } from "@/lib/commands/timeline/element/insert-element"; @@ -288,6 +289,11 @@ export class TimelineManager { this.editor.command.execute({ command }); } + generateTemplateCut({ tracks }: { tracks: TimelineTrack[] }): void { + const command = new GenerateTemplateCutCommand(tracks); + this.editor.command.execute({ command }); + } + // ---- Transition management ---- addTransition({ diff --git a/apps/web/src/hooks/actions/use-editor-actions.ts b/apps/web/src/hooks/actions/use-editor-actions.ts index a5b7e3a..a047a74 100644 --- a/apps/web/src/hooks/actions/use-editor-actions.ts +++ b/apps/web/src/hooks/actions/use-editor-actions.ts @@ -9,6 +9,7 @@ import { getElementsAtTime } from "@/lib/timeline"; import { generateAndInsertSpeech } from "@/lib/tts/service"; import { toast } from "sonner"; import { i18next } from "@/lib/i18n"; +import { generateTemplateCut as generateTemplateCutDraft } from "@/lib/auto-edit/generate-template-cut"; export function useEditorActions() { const editor = useEditor(); @@ -127,6 +128,40 @@ export function useEditorActions() { undefined, ); + useActionHandler( + "generate-template-cut", + (args) => { + if (!activeProject) { + toast.error(i18next.t("No active project")); + return; + } + + try { + const { tracks } = generateTemplateCutDraft({ + templateId: args.templateId, + assets: editor.media.getAssets(), + }); + + if (editor.playback.getIsPlaying()) { + editor.playback.toggle(); + } + + editor.timeline.generateTemplateCut({ tracks }); + editor.selection.clearSelection(); + editor.playback.seek({ time: 0 }); + + toast.success(i18next.t("Template draft generated")); + } catch (error) { + toast.error( + error instanceof Error + ? i18next.t(error.message) + : i18next.t("Failed to generate template draft"), + ); + } + }, + undefined, + ); + useActionHandler( "split", () => { diff --git a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts index 80f7f07..f1e524b 100644 --- a/apps/web/src/hooks/use-keyboard-shortcuts-help.ts +++ b/apps/web/src/hooks/use-keyboard-shortcuts-help.ts @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { useKeybindingsStore } from "@/stores/keybindings-store"; -import { ACTIONS, type TAction } from "@/lib/actions"; +import { ACTIONS, type TActionWithOptionalArgs } from "@/lib/actions"; import { getPlatformAlternateKey, getPlatformSpecialKey, @@ -13,7 +13,7 @@ export interface KeyboardShortcut { keys: string[]; description: string; category: string; - action: TAction; + action: TActionWithOptionalArgs; icon?: React.ReactNode; } @@ -40,7 +40,7 @@ export function useKeyboardShortcutsHelp() { const shortcuts = useMemo(() => { const result: KeyboardShortcut[] = []; - const actionToKeys: Record = {}; + const actionToKeys: Partial> = {}; for (const [key, action] of Object.entries(keybindings)) { if (action) { @@ -51,8 +51,10 @@ export function useKeyboardShortcutsHelp() { } } - for (const [actionId, keys] of Object.entries(actionToKeys)) { - if (!isAction(actionId)) continue; + for (const [actionId, keys] of Object.entries(actionToKeys) as Array< + [TActionWithOptionalArgs, string[] | undefined] + >) { + if (!keys) continue; const actionDef = ACTIONS[actionId]; result.push({ @@ -76,7 +78,3 @@ export function useKeyboardShortcutsHelp() { shortcuts, }; } - -function isAction(id: string): id is TAction { - return id in ACTIONS; -} diff --git a/apps/web/src/lib/actions/definitions.ts b/apps/web/src/lib/actions/definitions.ts index b248262..ddb60c5 100644 --- a/apps/web/src/lib/actions/definitions.ts +++ b/apps/web/src/lib/actions/definitions.ts @@ -131,6 +131,11 @@ export const ACTIONS = { description: "Convert text to speech", category: "editing", }, + "generate-template-cut": { + description: "Generate a template-based video draft", + category: "editing", + args: { templateId: "string" }, + }, "toggle-bookmark": { description: "Toggle bookmark at playhead", category: "timeline", diff --git a/apps/web/src/lib/actions/types.ts b/apps/web/src/lib/actions/types.ts index 508bf10..09302b7 100644 --- a/apps/web/src/lib/actions/types.ts +++ b/apps/web/src/lib/actions/types.ts @@ -8,6 +8,7 @@ export type TActionArgsMap = { "seek-backward": { seconds: number } | undefined; "jump-forward": { seconds: number } | undefined; "jump-backward": { seconds: number } | undefined; + "generate-template-cut": { templateId: "clean-cut" | "story-pulse" | "memory-album" }; }; type TKeysWithValueUndefined = { diff --git a/apps/web/src/lib/auto-edit/generate-template-cut.test.ts b/apps/web/src/lib/auto-edit/generate-template-cut.test.ts new file mode 100644 index 0000000..7859144 --- /dev/null +++ b/apps/web/src/lib/auto-edit/generate-template-cut.test.ts @@ -0,0 +1,167 @@ +import { describe, expect, test } from "bun:test"; +import { EditorCore } from "@/core"; +import { buildDefaultScene } from "@/lib/scenes"; +import type { MediaAsset } from "@/types/assets"; +import type { TProject } from "@/types/project"; +import { generateTemplateCut } from "./generate-template-cut"; + +const createFile = ({ + name, + type, +}: { + name: string; + type: string; +}) => new File(["test"], name, { type }); + +const createImageAsset = (name: string): MediaAsset => ({ + id: `image-${name}`, + name, + type: "image", + file: createFile({ name, type: "image/png" }), + width: 1920, + height: 1080, +}); + +const createVideoAsset = (name: string, duration: number): MediaAsset => ({ + id: `video-${name}`, + name, + type: "video", + file: createFile({ name, type: "video/mp4" }), + duration, + width: 1920, + height: 1080, + fps: 30, +}); + +const createAudioAsset = (name: string, duration: number): MediaAsset => ({ + id: `audio-${name}`, + name, + type: "audio", + file: createFile({ name, type: "audio/mp3" }), + duration, +}); + +describe("generateTemplateCut", () => { + test("creates a visual main track and text track from visual assets", () => { + const result = generateTemplateCut({ + templateId: "clean-cut", + assets: [createImageAsset("cover.png"), createVideoAsset("clip.mp4", 8)], + }); + + expect(result.tracks.map((track) => track.type)).toEqual(["video", "text"]); + expect(result.tracks[0]?.elements).toHaveLength(2); + expect(result.transitions).toHaveLength(1); + }); + + test("adds a dedicated audio track when audio assets are available", () => { + const result = generateTemplateCut({ + templateId: "story-pulse", + assets: [createImageAsset("cover.png"), createAudioAsset("bgm.mp3", 12)], + }); + + expect(result.tracks.map((track) => track.type)).toEqual([ + "video", + "text", + "audio", + ]); + expect(result.tracks[2]?.elements).toHaveLength(1); + }); + + test("uses template-specific pacing and transitions", () => { + const cleanCut = generateTemplateCut({ + templateId: "clean-cut", + assets: [createImageAsset("cover.png"), createImageAsset("detail.png")], + }); + const storyPulse = generateTemplateCut({ + templateId: "story-pulse", + assets: [createImageAsset("cover.png"), createImageAsset("detail.png")], + }); + + expect(cleanCut.tracks[0]?.elements[0]?.duration).toBe(3.5); + expect(storyPulse.tracks[0]?.elements[0]?.duration).toBe(2.5); + expect(cleanCut.transitions[0]?.type).toBe("dissolve"); + expect(storyPulse.transitions[0]?.type).toBe("wipe-left"); + }); + + test("throws when no visual assets are provided", () => { + expect(() => + generateTemplateCut({ + templateId: "clean-cut", + assets: [createAudioAsset("voice.mp3", 5)], + }), + ).toThrow("No visual assets"); + }); + + test("applies generated tracks through the command stack and supports undo", () => { + const testWindow = { + setTimeout, + clearTimeout, + addEventListener: () => {}, + removeEventListener: () => {}, + } as unknown as Window & typeof globalThis; + const globalWithWindow = globalThis as typeof globalThis & { + window?: Window & typeof globalThis; + }; + const originalWindow = globalWithWindow.window; + + globalWithWindow.window = testWindow; + + try { + EditorCore.reset(); + const editor = EditorCore.getInstance(); + const mainScene = buildDefaultScene({ + name: "Main scene", + isMain: true, + }); + const initialTracks = mainScene.tracks; + const project: TProject = { + metadata: { + id: "project-1", + name: "Project 1", + duration: 0, + createdAt: new Date("2026-03-17T00:00:00.000Z"), + updatedAt: new Date("2026-03-17T00:00:00.000Z"), + }, + scenes: [mainScene], + currentSceneId: mainScene.id, + settings: { + fps: 30, + canvasSize: { + width: 1920, + height: 1080, + }, + originalCanvasSize: null, + background: { + type: "color", + color: "#000000", + }, + }, + version: 3, + }; + + editor.project.setActiveProject({ project }); + editor.scenes.initializeScenes({ + scenes: project.scenes, + currentSceneId: project.currentSceneId, + }); + + const { tracks } = generateTemplateCut({ + templateId: "clean-cut", + assets: [createImageAsset("cover.png")], + }); + + editor.timeline.generateTemplateCut({ tracks }); + expect(editor.timeline.getTracks()).toEqual(tracks); + + editor.command.undo(); + expect(editor.timeline.getTracks()).toEqual(initialTracks); + } finally { + EditorCore.reset(); + if (originalWindow) { + globalWithWindow.window = originalWindow; + } else { + delete globalWithWindow.window; + } + } + }); +}); diff --git a/apps/web/src/lib/auto-edit/generate-template-cut.ts b/apps/web/src/lib/auto-edit/generate-template-cut.ts new file mode 100644 index 0000000..7bab88a --- /dev/null +++ b/apps/web/src/lib/auto-edit/generate-template-cut.ts @@ -0,0 +1,259 @@ +import { buildTrackTransition } from "@/lib/timeline/transition-utils"; +import { + buildImageElement, + buildUploadAudioElement, + buildVideoElement, +} from "@/lib/timeline/element-utils"; +import type { MediaAsset } from "@/types/assets"; +import type { + AudioElement, + AudioTrack, + ImageElement, + TextTrack, + TextElement, + TimelineTrack, + TrackTransition, + VideoElement, + VideoTrack, +} from "@/types/timeline"; +import { generateUUID } from "@/utils/id"; +import { + getTemplateCut, + type TemplateCutDefinition, + type TemplateCutId, +} from "./templates"; + +export interface GeneratedTemplateCut { + template: TemplateCutDefinition; + tracks: TimelineTrack[]; + transitions: TrackTransition[]; +} + +const INTRO_DURATION = 2.5; +const OUTRO_DURATION = 2.5; + +export function generateTemplateCut({ + templateId, + assets, +}: { + templateId: TemplateCutId; + assets: MediaAsset[]; +}): GeneratedTemplateCut { + const template = getTemplateCut({ templateId }); + const visualAssets = assets.filter( + (asset) => !asset.ephemeral && (asset.type === "image" || asset.type === "video"), + ); + + if (visualAssets.length === 0) { + throw new Error("No visual assets"); + } + + let currentTime = 0; + const visualElements: Array = visualAssets.map( + (asset) => { + const startTime = currentTime; + const duration = + asset.type === "image" + ? template.imageDuration + : clampDuration({ + duration: asset.duration ?? template.maxVideoDuration, + min: template.minVideoDuration, + max: template.maxVideoDuration, + }); + currentTime += duration; + + if (asset.type === "video") { + return { + ...buildVideoElement({ + mediaId: asset.id, + name: asset.name, + duration, + startTime, + }), + id: generateUUID(), + }; + } + + return { + ...buildImageElement({ + mediaId: asset.id, + name: asset.name, + duration, + startTime, + }), + id: generateUUID(), + }; + }, + ); + + const transitions = visualElements.flatMap((element, index) => { + const nextElement = visualElements[index + 1]; + if (!nextElement) { + return []; + } + + return [ + buildTrackTransition({ + type: template.transitionType, + duration: template.transitionDuration, + fromElementId: element.id, + toElementId: nextElement.id, + }), + ]; + }); + + const visualTrack: VideoTrack = { + id: generateUUID(), + name: `${template.name} Visuals`, + type: "video", + elements: visualElements, + transitions, + isMain: true, + muted: false, + hidden: false, + }; + + const totalDuration = Math.max( + currentTime, + INTRO_DURATION + OUTRO_DURATION, + ); + + const textTrack: TextTrack = { + id: generateUUID(), + name: `${template.name} Titles`, + type: "text", + hidden: false, + elements: [ + buildTemplateTextElement({ + name: `${template.name} Intro`, + content: template.introText, + startTime: 0, + duration: INTRO_DURATION, + fontSize: 20, + fontWeight: "bold", + backgroundColor: "#111111cc", + backgroundPaddingX: 14, + backgroundPaddingY: 8, + backgroundBorderRadius: 10, + positionY: -32, + }), + buildTemplateTextElement({ + name: `${template.name} Outro`, + content: template.outroText, + startTime: Math.max(totalDuration - OUTRO_DURATION, 0), + duration: OUTRO_DURATION, + fontSize: 16, + backgroundColor: "#11111199", + backgroundPaddingX: 12, + backgroundPaddingY: 6, + backgroundBorderRadius: 10, + positionY: 36, + }), + ], + }; + + const tracks: TimelineTrack[] = [visualTrack, textTrack]; + const audioAssets = assets.filter( + (asset) => !asset.ephemeral && asset.type === "audio", + ); + + if (audioAssets.length > 0) { + let audioStartTime = 0; + const audioTrack: AudioTrack = { + id: generateUUID(), + name: `${template.name} Audio`, + type: "audio", + muted: false, + elements: audioAssets.map((asset): AudioElement => { + const duration = asset.duration ?? template.defaultAudioDuration; + const element = { + ...buildUploadAudioElement({ + mediaId: asset.id, + name: asset.name, + duration, + startTime: audioStartTime, + }), + id: generateUUID(), + }; + audioStartTime += duration; + return element; + }), + }; + tracks.push(audioTrack); + } + + return { + template, + tracks, + transitions, + }; +} + +function clampDuration({ + duration, + min, + max, +}: { + duration: number; + min: number; + max: number; +}): number { + return Math.max(min, Math.min(max, duration)); +} + +function buildTemplateTextElement({ + name, + content, + startTime, + duration, + fontSize, + fontWeight = "normal", + backgroundColor, + backgroundPaddingX, + backgroundPaddingY, + backgroundBorderRadius, + positionY, +}: { + name: string; + content: string; + startTime: number; + duration: number; + fontSize: number; + fontWeight?: TextElement["fontWeight"]; + backgroundColor: string; + backgroundPaddingX: number; + backgroundPaddingY: number; + backgroundBorderRadius: number; + positionY: number; +}): TextElement { + return { + id: generateUUID(), + type: "text", + name, + content, + duration, + startTime, + trimStart: 0, + trimEnd: 0, + fontSize, + fontFamily: "Arial", + color: "#ffffff", + backgroundColor, + textAlign: "center", + fontWeight, + fontStyle: "normal", + textDecoration: "none", + transform: { + scale: 1, + position: { + x: 0, + y: positionY, + }, + rotate: 0, + }, + opacity: 1, + backgroundPaddingX, + backgroundPaddingY, + backgroundBorderRadius, + }; +} diff --git a/apps/web/src/lib/auto-edit/templates.ts b/apps/web/src/lib/auto-edit/templates.ts new file mode 100644 index 0000000..cb326a7 --- /dev/null +++ b/apps/web/src/lib/auto-edit/templates.ts @@ -0,0 +1,71 @@ +import type { TransitionType } from "@/types/timeline"; + +export type TemplateCutId = "clean-cut" | "story-pulse" | "memory-album"; + +export interface TemplateCutDefinition { + id: TemplateCutId; + name: string; + description: string; + imageDuration: number; + defaultAudioDuration: number; + maxVideoDuration: number; + minVideoDuration: number; + transitionType: TransitionType; + transitionDuration: number; + introText: string; + outroText: string; +} + +export const TEMPLATE_CUTS: TemplateCutDefinition[] = [ + { + id: "clean-cut", + name: "Clean Cut", + description: "Balanced pacing with simple dissolves.", + imageDuration: 3.5, + defaultAudioDuration: 12, + maxVideoDuration: 6, + minVideoDuration: 2.5, + transitionType: "dissolve", + transitionDuration: 0.35, + introText: "Clean Cut", + outroText: "Keep editing", + }, + { + id: "story-pulse", + name: "Story Pulse", + description: "Faster pacing with directional wipes.", + imageDuration: 2.5, + defaultAudioDuration: 10, + maxVideoDuration: 4, + minVideoDuration: 2, + transitionType: "wipe-left", + transitionDuration: 0.3, + introText: "Story Pulse", + outroText: "Tune the rhythm", + }, + { + id: "memory-album", + name: "Memory Album", + description: "Longer holds for photos and softer endings.", + imageDuration: 4.5, + defaultAudioDuration: 14, + maxVideoDuration: 7, + minVideoDuration: 3, + transitionType: "wipe-up", + transitionDuration: 0.45, + introText: "Memory Album", + outroText: "Add your final touch", + }, +] as const; + +export function getTemplateCut({ + templateId, +}: { + templateId: TemplateCutId; +}): TemplateCutDefinition { + const template = TEMPLATE_CUTS.find((item) => item.id === templateId); + if (!template) { + throw new Error(`Unknown template: ${templateId}`); + } + return template; +} diff --git a/apps/web/src/lib/commands/timeline/index.ts b/apps/web/src/lib/commands/timeline/index.ts index 30bc81e..54b1d71 100644 --- a/apps/web/src/lib/commands/timeline/index.ts +++ b/apps/web/src/lib/commands/timeline/index.ts @@ -1,3 +1,4 @@ export * from "./track"; export * from "./element"; export * from "./clipboard"; +export * from "./template"; diff --git a/apps/web/src/lib/commands/timeline/template/generate-template-cut.ts b/apps/web/src/lib/commands/timeline/template/generate-template-cut.ts new file mode 100644 index 0000000..9c76752 --- /dev/null +++ b/apps/web/src/lib/commands/timeline/template/generate-template-cut.ts @@ -0,0 +1,26 @@ +import { EditorCore } from "@/core"; +import { Command } from "@/lib/commands/base-command"; +import type { TimelineTrack } from "@/types/timeline"; + +export class GenerateTemplateCutCommand extends Command { + private savedState: TimelineTrack[] | null = null; + + constructor(private readonly tracks: TimelineTrack[]) { + super(); + } + + execute(): void { + const editor = EditorCore.getInstance(); + this.savedState = structuredClone(editor.timeline.getTracks()); + editor.timeline.updateTracks(this.tracks); + } + + undo(): void { + if (!this.savedState) { + return; + } + + const editor = EditorCore.getInstance(); + editor.timeline.updateTracks(this.savedState); + } +} diff --git a/apps/web/src/lib/commands/timeline/template/index.ts b/apps/web/src/lib/commands/timeline/template/index.ts new file mode 100644 index 0000000..ca53fb5 --- /dev/null +++ b/apps/web/src/lib/commands/timeline/template/index.ts @@ -0,0 +1 @@ +export * from "./generate-template-cut"; diff --git a/apps/web/src/stores/keybindings-store.ts b/apps/web/src/stores/keybindings-store.ts index 841a75f..a3c4fbc 100644 --- a/apps/web/src/stores/keybindings-store.ts +++ b/apps/web/src/stores/keybindings-store.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { persist } from "zustand/middleware"; -import type { TActionWithOptionalArgs } from "@/lib/actions"; +import type { TAction, TActionWithOptionalArgs } from "@/lib/actions"; import { getDefaultShortcuts } from "@/lib/actions"; import { isTypableDOMElement } from "@/utils/browser"; import { isAppleDevice } from "@/utils/platform"; @@ -12,7 +12,15 @@ import { CURRENT_VERSION, } from "./keybindings/migrations"; -export const defaultKeybindings: KeybindingConfig = getDefaultShortcuts(); +const isKeybindableAction = ( + action: TAction, +): action is TActionWithOptionalArgs => action !== "generate-template-cut"; + +export const defaultKeybindings: KeybindingConfig = Object.fromEntries( + Object.entries(getDefaultShortcuts()).filter(([, action]) => + isKeybindableAction(action), + ), +) as KeybindingConfig; export interface KeybindingConflict { key: ShortcutKey; diff --git a/docs/plans/2026-03-17-one-click-video-design.md b/docs/plans/2026-03-17-one-click-video-design.md new file mode 100644 index 0000000..373a1b4 --- /dev/null +++ b/docs/plans/2026-03-17-one-click-video-design.md @@ -0,0 +1,204 @@ +# TIA-52 一键成片设计说明 + +## 背景 + +当前编辑器已经具备以下能力: + +- `MediaView` 支持导入图片、视频、音频素材,并手动拖入时间线。 +- `TimelineManager`、`element-utils`、`transition-utils` 已具备轨道、元素、转场拼装能力。 +- `AIView` 已承载“生成图片/视频”“角色”“历史”等自动化能力,是最接近“一键成片”的现有入口。 + +当前缺口同样明确: + +- 没有固定风格模板。 +- 没有基于当前素材自动生成时间线草稿的流程。 +- 没有可撤销的“整条时间线替换”命令。 + +## 复现信号 + +- [apps/web/src/components/editor/panels/assets/views/media.tsx](/root/.config/superpowers/worktrees/cutia/.symphony-workspaces/symphony-full-workflow/TIA-52/apps/web/src/components/editor/panels/assets/views/media.tsx) 只支持导入素材和单个添加到时间线。 +- [apps/web/src/components/editor/panels/assets/views/ai.tsx](/root/.config/superpowers/worktrees/cutia/.symphony-workspaces/symphony-full-workflow/TIA-52/apps/web/src/components/editor/panels/assets/views/ai.tsx) 目前只有 `Generate`、`Characters`、`History` 三个子视图。 +- [apps/web/src/lib/actions/definitions.ts](/root/.config/superpowers/worktrees/cutia/.symphony-workspaces/symphony-full-workflow/TIA-52/apps/web/src/lib/actions/definitions.ts) 中不存在与模板成片相关的 action。 + +## 方案比较 + +### 方案 A:新增左侧一级面板 Tab + +优点: + +- 可见性最高。 +- 模板能力和 AI 生成解耦。 + +缺点: + +- 需要改动 `assets-panel-store`、`tabbar`、面板映射,UI 侵入面较大。 +- 对当前“AI 辅助编辑能力集中在 AI 面板”这一信息架构不够一致。 + +### 方案 B:在 AI 面板中新增“模板成片”子视图 + +优点: + +- 只改动 `AIView` 内部结构,落点集中。 +- 用户已经会在 AI 面板寻找自动化能力,符合心理预期。 +- 可以直接复用当前项目素材、toast、动作系统。 + +缺点: + +- 入口比一级 Tab 稍深一层。 + +### 方案 C:在顶部 Header 放一个“一键成片”按钮并弹出模态框 + +优点: + +- 操作最显眼。 + +缺点: + +- Header 已较拥挤,增加后会稀释现有主操作。 +- 模态框仍需承载模板说明、素材校验和覆盖确认,复杂度并不比 AI 面板低。 + +## 结论 + +采用 **方案 B**: + +- 在 `AIView` 中新增“模板成片”子视图。 +- 用户从当前项目素材库中导入素材后,进入该视图选择模板。 +- 模板生成逻辑由纯函数负责,UI 只收集选择并触发 action。 +- 真正的时间线替换通过新命令完成,保证撤销/重做成立。 + +## 功能边界 + +本次只做固定模板的一键成片草稿,不做: + +- AI 分镜理解。 +- 自动配乐推荐。 +- 素材级勾选筛选器。 +- 智能识别人脸、语音节拍或字幕生成联动。 + +默认规则: + +- 使用当前项目素材库中全部非临时素材。 +- 视觉素材仅取 `image` / `video`。 +- 音频素材仅取 `audio`,作为可选背景音轨顺序铺开。 +- 若当前时间线已有内容,生成前要求确认覆盖。 + +## 模板模型 + +定义固定模板配置,至少包含: + +- `id` / `name` / `description` +- `pace`: 视频和图片的目标节奏 +- `imageDuration` +- `videoDurationPolicy`: 保留原时长、截短上限、最小时长兜底 +- `transition`: 类型与时长 +- `heroText`: 首屏文案样式 +- `closingText`: 收尾文案样式 +- `visualMotion`: 对图片/视频施加的轻量缩放与位移节奏 + +首版提供 3 个模板: + +1. `clean-cut` + - 节奏均衡 + - `dissolve` 转场 + - 适合通用展示 +2. `story-pulse` + - 节奏更快 + - `wipe-left` 转场 + - 适合 vlog / 节奏剪辑 +3. `memory-album` + - 图片停留更久 + - `fade` 风格收尾文案 + - 适合相册型内容 + +## 数据流 + +1. 用户在媒体面板导入素材。 +2. 在 AI 面板切换到“模板成片”。 +3. 选择一个固定模板并点击生成。 +4. UI 通过 `invokeAction("generate-template-cut", { templateId })` 触发动作。 +5. `use-editor-actions` 中的 handler: + - 读取当前项目与素材。 + - 校验是否存在可用视觉素材。 + - 若当前时间线非空且用户尚未确认覆盖,则中断并提示。 + - 调用纯函数生成 `TimelineTrack[]`。 + - 执行新命令替换 active scene tracks。 +6. 编辑器现有订阅链触发预览刷新、保存和后续编辑。 + +## 时间线生成规则 + +### 视觉轨 + +- 主视频轨始终存在,并承载全部图片/视频元素。 +- 图片使用模板定义的默认时长。 +- 视频默认保留原始时长,但根据模板节奏截断到统一上限,避免过长素材破坏节奏。 +- 每个元素串行排列,`startTime` 取前一元素结束时间。 + +### 文本轨 + +- 生成一个头部标题元素,内容取模板名称或模板预设文案。 +- 生成一个尾部收尾元素,提示“继续编辑”或模板 tagline。 +- 文本样式由模板定义,保证模板风格差异可见。 + +### 音频轨 + +- 若存在音频素材,则创建一条音频轨并从 `0s` 开始顺序铺开。 +- 若不存在音频素材,不额外补充占位音轨。 + +### 转场 + +- 在主视频轨相邻视觉元素之间补转场。 +- 使用现有 `transition-utils` 的相邻元素约束,不做重叠式复杂调度。 + +## 命令与撤销 + +新增“模板成片替换时间线”命令: + +- `execute()` 保存当前 tracks,再写入新 tracks。 +- `undo()` 恢复旧 tracks。 + +这样可以满足: + +- 用户点击后立刻看到结果。 +- `undo/redo` 保持可用。 +- 不需要把整个流程拆成几十个小命令再组合。 + +## UI 交互 + +`AIView` 新增一个 `Templates` 子视图,包含: + +- 模板卡片列表 +- 模板标签:节奏、适用素材数量、转场类型 +- “使用当前素材生成”按钮 +- 无视觉素材时展示空状态,并提供“去 Media 面板导入素材”按钮 + +若当前时间线已有任意元素: + +- 先弹出覆盖确认对话框。 +- 用户确认后才触发 action。 + +## 测试策略 + +### 单元测试 + +新增纯函数测试,验证: + +- 只传图片时能生成主视频轨 + 文本轨。 +- 同时传图片/视频/音频时能生成三条轨道。 +- 模板差异会反映在元素时长、转场和文本上。 +- 没有视觉素材时返回错误。 + +### 命令测试 + +本次不强行上复杂 EditorCore 集成测试,先通过纯函数测试覆盖主要编排逻辑。 + +### 构建验证 + +- `bun test` +- `bun run lint:web` +- `bun run build:web` + +## 风险与取舍 + +- 当前首版默认使用“全部素材”,没有素材选择器,适合快速草稿,不适合精细挑片。 +- 音频素材顺序铺开是保守策略,不尝试自动 beat sync。 +- 模板差异主要来自节奏、转场、文字和轻量动效,不做复杂视觉特效,避免超出当前编辑器能力边界。 diff --git a/docs/plans/2026-03-17-one-click-video.md b/docs/plans/2026-03-17-one-click-video.md new file mode 100644 index 0000000..ac534bb --- /dev/null +++ b/docs/plans/2026-03-17-one-click-video.md @@ -0,0 +1,280 @@ +# 一键成片功能 Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** 在编辑器 AI 面板中提供固定模板入口,基于当前已导入素材一键生成可继续编辑的时间线草稿,并支持撤销。 + +**Architecture:** 通过纯函数生成模板化 `TimelineTrack[]`,再用一个新的 timeline command 原子替换 active scene 的 tracks。UI 只负责展示模板、校验素材和发起 action,避免把编排逻辑塞进 React 组件。 + +**Tech Stack:** Next.js App Router, React 19, Zustand, EditorCore/Timeline commands, Bun test, Biome + +--- + +## Task 1: 建立一键成片生成器的测试骨架 + +**Files:** +- Create: `apps/web/src/lib/auto-edit/generate-template-cut.test.ts` +- Create: `apps/web/src/lib/auto-edit/templates.ts` +- Create: `apps/web/src/lib/auto-edit/generate-template-cut.ts` + +**Step 1: Write the failing test** + +```typescript +import { describe, expect, test } from "bun:test"; +import { generateTemplateCut } from "./generate-template-cut"; + +describe("generateTemplateCut", () => { + test("使用视觉素材生成主轨和文本轨", () => { + const result = generateTemplateCut({ + templateId: "clean-cut", + assets: [ + createImageAsset("cover.png"), + createVideoAsset("clip.mp4", 8), + ], + }); + + expect(result.tracks.map((track) => track.type)).toEqual(["video", "text"]); + expect(result.tracks[0]?.elements).toHaveLength(2); + expect(result.transitions).toHaveLength(1); + }); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/web/src/lib/auto-edit/generate-template-cut.test.ts` +Expected: FAIL with missing module / missing export / behavior mismatch + +**Step 3: Write minimal implementation** + +- 新建模板定义文件,声明 `clean-cut`、`story-pulse`、`memory-album` +- 新建生成器纯函数,先满足: + - 能筛出视觉素材 + - 能生成串行 video track + - 能生成 intro/outro text track + - 能产出 transition 描述 + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/web/src/lib/auto-edit/generate-template-cut.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/src/lib/auto-edit/generate-template-cut.test.ts apps/web/src/lib/auto-edit/templates.ts apps/web/src/lib/auto-edit/generate-template-cut.ts +git commit -m "test: add template cut generator coverage" +``` + +## Task 2: 补充混合素材与错误分支测试 + +**Files:** +- Modify: `apps/web/src/lib/auto-edit/generate-template-cut.test.ts` +- Modify: `apps/web/src/lib/auto-edit/generate-template-cut.ts` + +**Step 1: Write the failing test** + +```typescript +test("存在音频素材时生成独立音轨", () => { + const result = generateTemplateCut({ + templateId: "story-pulse", + assets: [ + createImageAsset("a.png"), + createAudioAsset("bgm.mp3", 12), + ], + }); + + expect(result.tracks.map((track) => track.type)).toEqual(["video", "text", "audio"]); +}); + +test("没有视觉素材时返回可展示错误", () => { + expect(() => + generateTemplateCut({ + templateId: "clean-cut", + assets: [createAudioAsset("voice.mp3", 5)], + }), + ).toThrow("No visual assets"); +}); +``` + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/web/src/lib/auto-edit/generate-template-cut.test.ts` +Expected: FAIL on missing audio track/error handling + +**Step 3: Write minimal implementation** + +- 在生成器中加入: + - 音频轨生成逻辑 + - 模板 pace 差异 + - `No visual assets` 异常 + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/web/src/lib/auto-edit/generate-template-cut.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/src/lib/auto-edit/generate-template-cut.test.ts apps/web/src/lib/auto-edit/generate-template-cut.ts +git commit -m "feat: support mixed assets in template cuts" +``` + +## Task 3: 为模板成片引入可撤销命令 + +**Files:** +- Create: `apps/web/src/lib/commands/timeline/template/generate-template-cut.ts` +- Create: `apps/web/src/lib/commands/timeline/template/index.ts` +- Modify: `apps/web/src/lib/commands/timeline/index.ts` +- Modify: `apps/web/src/core/managers/timeline-manager.ts` + +**Step 1: Write the failing test** + +```typescript +test("生成命令可用 undo 恢复原始轨道", () => { + // 保存旧 tracks,执行命令后变成新 tracks,再 undo 恢复 +}); +``` + +如果集成测试成本过高,则改为先写命令使用点并让 TypeScript/构建失败,作为红灯。 + +**Step 2: Run test to verify it fails** + +Run: `bun test apps/web/src/lib/auto-edit/generate-template-cut.test.ts` +Expected: FAIL or `bun run build:web` 出现缺少命令导出/方法的问题 + +**Step 3: Write minimal implementation** + +- 新命令保存旧 tracks 与新 tracks +- TimelineManager 暴露 `generateTemplateCut({ tracks })` 或同等语义方法 +- 通过 CommandManager 执行,确保 undo/redo 可用 + +**Step 4: Run test to verify it passes** + +Run: `bun test apps/web/src/lib/auto-edit/generate-template-cut.test.ts` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/src/lib/commands/timeline/template apps/web/src/lib/commands/timeline/index.ts apps/web/src/core/managers/timeline-manager.ts +git commit -m "feat: add undoable template cut command" +``` + +## Task 4: 接入动作系统 + +**Files:** +- Modify: `apps/web/src/lib/actions/definitions.ts` +- Modify: `apps/web/src/lib/actions/types.ts` +- Modify: `apps/web/src/hooks/actions/use-editor-actions.ts` + +**Step 1: Write the failing test** + +如果没有现成 action 测试基建,则先让编译失败: + +```typescript +invokeAction("generate-template-cut", { templateId: "clean-cut" }); +``` + +并让 TypeScript 报错“未知 action”作为红灯。 + +**Step 2: Run test to verify it fails** + +Run: `bun run build:web` +Expected: FAIL because action/type/handler missing + +**Step 3: Write minimal implementation** + +- 添加 `generate-template-cut` action 定义 +- 在 args map 中声明 `{ templateId: string }` +- 在 `useEditorActions` 中: + - 读取 active project / media assets / timeline tracks + - 调用生成器 + - 执行模板替换命令 + - 用 toast 反馈错误和成功 + +**Step 4: Run test to verify it passes** + +Run: `bun run build:web` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/src/lib/actions/definitions.ts apps/web/src/lib/actions/types.ts apps/web/src/hooks/actions/use-editor-actions.ts +git commit -m "feat: wire template cut action" +``` + +## Task 5: 在 AI 面板实现模板成片 UI + +**Files:** +- Modify: `apps/web/src/components/editor/panels/assets/views/ai.tsx` + +**Step 1: Write the failing test** + +若缺少 React 测试基础,则先从构建红灯开始: + +- 在 UI 中引用尚未存在的模板定义/状态 +- 确认 `bun run build:web` 报错 + +**Step 2: Run test to verify it fails** + +Run: `bun run build:web` +Expected: FAIL because UI references are incomplete + +**Step 3: Write minimal implementation** + +- 在 `AIView` 增加 `Templates` 子视图 +- 渲染 3 个模板卡片 +- 无视觉素材时展示空状态与“去 Media 面板”按钮 +- 生成按钮调用 `invokeAction("generate-template-cut", { templateId })` +- 若当前时间线非空,先弹确认框再执行 + +**Step 4: Run test to verify it passes** + +Run: `bun run build:web` +Expected: PASS + +**Step 5: Commit** + +```bash +git add apps/web/src/components/editor/panels/assets/views/ai.tsx +git commit -m "feat: add one-click template panel" +``` + +## Task 6: 同步翻译与全量验证 + +**Files:** +- Modify: `apps/web/public/locales/*/translation.json` (如工具自动更新) + +**Step 1: Write the failing test** + +不额外写新测试,直接执行全量校验作为红灯。 + +**Step 2: Run test to verify it fails** + +Run: `bun run lint:web` +Expected: 如有新字符串未提取、lint 问题或类型问题则先失败 + +**Step 3: Write minimal implementation** + +- 运行 `bun run translation:extract`(若新增文案) +- 修正 lint / formatting / type issues + +**Step 4: Run test to verify it passes** + +Run: +- `bun test` +- `bun run lint:web` +- `bun run build:web` + +Expected: +- 全部退出码为 0 + +**Step 5: Commit** + +```bash +git add docs/plans apps/web/src apps/web/public/locales +git commit -m "feat: add one-click template video drafts" +```