chore(messages): update media generation prompts in multiple languages
This commit is contained in:
@@ -42,3 +42,13 @@ NEXTAUTH_SECRET="your-super-secret-key-change-in-production"
|
||||
|
||||
# Logging (optional)
|
||||
# LOG_LEVEL="info" # Options: trace, debug, info, warn, error, fatal
|
||||
|
||||
# Media Generation - Wiro.ai (optional)
|
||||
# WIRO_API_KEY=your_wiro_api_key
|
||||
# WIRO_VIDEO_MODELS="google/veo3.1-fast" # Comma-separated list of video models
|
||||
# WIRO_IMAGE_MODELS="google/nano-banana-pro,google/nano-banana" # Comma-separated list of image models
|
||||
|
||||
# Media Generation - Fal.ai (optional - currently disabled)
|
||||
# FAL_API_KEY=your_fal_api_key
|
||||
# FAL_VIDEO_MODELS="" # Comma-separated list of video models
|
||||
# FAL_IMAGE_MODELS="" # Comma-separated list of image models
|
||||
@@ -267,6 +267,43 @@
|
||||
"videoTooLarge": "الفيديو كبير جداً. الحد الأقصى للحجم هو 4 ميجابايت.",
|
||||
"invalidFileType": "نوع ملف غير صالح. يُسمح فقط بـ JPEG و PNG و GIF و WebP.",
|
||||
"invalidVideoType": "نوع فيديو غير صالح. يُسمح فقط بفيديوهات MP4.",
|
||||
"uploadMedia": "رفع الوسائط",
|
||||
"generateMedia": "إنشاء",
|
||||
"chooseGenerator": "اختر المولد",
|
||||
"uploadInstead": "الرفع بدلاً من ذلك",
|
||||
"confirmGeneration": "تأكيد الإنشاء",
|
||||
"confirmGenerationDescription": "إنشاء وسائط باستخدام {provider} مع نموذج {model}؟",
|
||||
"promptPreview": "معاينة الأمر",
|
||||
"noPromptProvided": "لم يتم تقديم أمر",
|
||||
"inputImage": "صورة الإدخال",
|
||||
"aspectRatio": "نسبة العرض إلى الارتفاع",
|
||||
"startGeneration": "بدء الإنشاء",
|
||||
"generatingMedia": "جارٍ إنشاء الوسائط",
|
||||
"doNotCloseWindow": "يرجى عدم إغلاق هذه النافذة أثناء الإنشاء.",
|
||||
"generationComplete": "اكتمل الإنشاء",
|
||||
"generationFailed": "فشل الإنشاء",
|
||||
"mediaAddedToPrompt": "تمت إضافة الوسائط إلى أمرك.",
|
||||
"mediaGenerated": "تم إنشاء الوسائط بنجاح!",
|
||||
"orGenerateMedia": "أو الإنشاء بالذكاء الاصطناعي",
|
||||
"close": "إغلاق",
|
||||
"mediaGeneration": {
|
||||
"connecting": "جارٍ الاتصال بالخادم...",
|
||||
"connected": "متصل، في انتظار الدور...",
|
||||
"queued": "في انتظار الدور...",
|
||||
"accepted": "تم قبول المهمة...",
|
||||
"preprocessStart": "جارٍ المعالجة المسبقة...",
|
||||
"preprocessEnd": "اكتملت المعالجة المسبقة",
|
||||
"gpuAssigned": "تم تعيين GPU، في انتظار الدور...",
|
||||
"started": "بدأ الإنشاء...",
|
||||
"generating": "جارٍ الإنشاء...",
|
||||
"processingOutput": "جارٍ معالجة المخرجات...",
|
||||
"ending": "اكتمل الإنشاء",
|
||||
"postprocessStart": "جارٍ المعالجة اللاحقة...",
|
||||
"postprocessEnd": "جارٍ الإنهاء...",
|
||||
"complete": "اكتمل!",
|
||||
"error": "حدث خطأ",
|
||||
"errorProcessing": "خطأ في المعالجة..."
|
||||
},
|
||||
"privateDescription": "الأوامر الخاصة مرئية لك فقط",
|
||||
"update": "تحديث",
|
||||
"createButton": "إنشاء",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "Video zu groß. Maximale Größe ist 4MB.",
|
||||
"invalidFileType": "Ungültiger Dateityp. Nur JPEG, PNG, GIF und WebP sind erlaubt.",
|
||||
"invalidVideoType": "Ungültiger Videotyp. Nur MP4-Videos sind erlaubt.",
|
||||
"uploadMedia": "Medien hochladen",
|
||||
"generateMedia": "Generieren",
|
||||
"chooseGenerator": "Generator wählen",
|
||||
"uploadInstead": "Stattdessen hochladen",
|
||||
"confirmGeneration": "Generierung bestätigen",
|
||||
"confirmGenerationDescription": "Medien mit {provider} und Modell {model} generieren?",
|
||||
"promptPreview": "Prompt-Vorschau",
|
||||
"noPromptProvided": "Kein Prompt angegeben",
|
||||
"inputImage": "Eingabebild",
|
||||
"aspectRatio": "Seitenverhältnis",
|
||||
"startGeneration": "Generierung starten",
|
||||
"generatingMedia": "Medien werden generiert",
|
||||
"doNotCloseWindow": "Bitte schließen Sie dieses Fenster nicht während der Generierung.",
|
||||
"generationComplete": "Generierung abgeschlossen",
|
||||
"generationFailed": "Generierung fehlgeschlagen",
|
||||
"mediaAddedToPrompt": "Medien wurden Ihrem Prompt hinzugefügt.",
|
||||
"mediaGenerated": "Medien erfolgreich generiert!",
|
||||
"orGenerateMedia": "Oder mit KI generieren",
|
||||
"close": "Schließen",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Verbindung zum Server...",
|
||||
"connected": "Verbunden, warte in Warteschlange...",
|
||||
"queued": "In Warteschlange...",
|
||||
"accepted": "Aufgabe akzeptiert...",
|
||||
"preprocessStart": "Vorverarbeitung...",
|
||||
"preprocessEnd": "Vorverarbeitung abgeschlossen",
|
||||
"gpuAssigned": "GPU zugewiesen, warte in Warteschlange...",
|
||||
"started": "Generierung gestartet...",
|
||||
"generating": "Generiere...",
|
||||
"processingOutput": "Ausgabe wird verarbeitet...",
|
||||
"ending": "Generierung abgeschlossen",
|
||||
"postprocessStart": "Nachbearbeitung...",
|
||||
"postprocessEnd": "Finalisierung...",
|
||||
"complete": "Fertig!",
|
||||
"error": "Fehler aufgetreten",
|
||||
"errorProcessing": "Verarbeitungsfehler..."
|
||||
},
|
||||
"privateDescription": "Private Prompts sind nur für Sie sichtbar",
|
||||
"requiresMediaUpload": "Medien-Upload erforderlich",
|
||||
"attachedMediaType": "Angehängter Medientyp",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "Το βίντεο είναι πολύ μεγάλο. Μέγιστο μέγεθος 4MB.",
|
||||
"invalidFileType": "Μη έγκυρος τύπος αρχείου. Επιτρέπονται μόνο JPEG, PNG, GIF και WebP.",
|
||||
"invalidVideoType": "Μη έγκυρος τύπος βίντεο. Επιτρέπονται μόνο βίντεο MP4.",
|
||||
"uploadMedia": "Ανέβασμα Μέσου",
|
||||
"generateMedia": "Δημιουργία",
|
||||
"chooseGenerator": "Επιλογή Γεννήτριας",
|
||||
"uploadInstead": "Ανέβασμα αντί αυτού",
|
||||
"confirmGeneration": "Επιβεβαίωση Δημιουργίας",
|
||||
"confirmGenerationDescription": "Δημιουργία μέσου με {provider} χρησιμοποιώντας το μοντέλο {model};",
|
||||
"promptPreview": "Προεπισκόπηση prompt",
|
||||
"noPromptProvided": "Δεν παρέχεται prompt",
|
||||
"inputImage": "Εικόνα εισόδου",
|
||||
"aspectRatio": "Αναλογία διαστάσεων",
|
||||
"startGeneration": "Έναρξη Δημιουργίας",
|
||||
"generatingMedia": "Δημιουργία Μέσου",
|
||||
"doNotCloseWindow": "Παρακαλώ μην κλείσετε αυτό το παράθυρο κατά τη δημιουργία.",
|
||||
"generationComplete": "Η Δημιουργία Ολοκληρώθηκε",
|
||||
"generationFailed": "Η Δημιουργία Απέτυχε",
|
||||
"mediaAddedToPrompt": "Το μέσο προστέθηκε στο prompt σας.",
|
||||
"mediaGenerated": "Το μέσο δημιουργήθηκε επιτυχώς!",
|
||||
"orGenerateMedia": "Ή δημιουργία με AI",
|
||||
"close": "Κλείσιμο",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Σύνδεση στον διακομιστή...",
|
||||
"connected": "Συνδέθηκε, αναμονή στην ουρά...",
|
||||
"queued": "Αναμονή στην ουρά...",
|
||||
"accepted": "Η εργασία έγινε αποδεκτή...",
|
||||
"preprocessStart": "Προεπεξεργασία...",
|
||||
"preprocessEnd": "Η προεπεξεργασία ολοκληρώθηκε",
|
||||
"gpuAssigned": "Εκχωρήθηκε GPU, αναμονή στην ουρά...",
|
||||
"started": "Η δημιουργία ξεκίνησε...",
|
||||
"generating": "Δημιουργία σε εξέλιξη...",
|
||||
"processingOutput": "Επεξεργασία εξόδου...",
|
||||
"ending": "Η δημιουργία ολοκληρώθηκε",
|
||||
"postprocessStart": "Μεταεπεξεργασία...",
|
||||
"postprocessEnd": "Οριστικοποίηση...",
|
||||
"complete": "Ολοκληρώθηκε!",
|
||||
"error": "Παρουσιάστηκε σφάλμα",
|
||||
"errorProcessing": "Σφάλμα επεξεργασίας..."
|
||||
},
|
||||
"privateDescription": "Τα ιδιωτικά prompts είναι ορατά μόνο σε εσάς",
|
||||
"requiresMediaUpload": "Απαιτεί ανέβασμα μέσου",
|
||||
"attachedMediaType": "Τύπος συνημμένου μέσου",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "Video too large. Maximum size is 4MB.",
|
||||
"invalidFileType": "Invalid file type. Only JPEG, PNG, GIF, and WebP are allowed.",
|
||||
"invalidVideoType": "Invalid video type. Only MP4 videos are allowed.",
|
||||
"uploadMedia": "Upload Media",
|
||||
"generateMedia": "Generate",
|
||||
"chooseGenerator": "Choose Generator",
|
||||
"uploadInstead": "Upload Instead",
|
||||
"confirmGeneration": "Confirm Generation",
|
||||
"confirmGenerationDescription": "Generate media using {provider} with model {model}?",
|
||||
"promptPreview": "Prompt preview",
|
||||
"noPromptProvided": "No prompt provided",
|
||||
"inputImage": "Input image",
|
||||
"aspectRatio": "Aspect Ratio",
|
||||
"startGeneration": "Start Generation",
|
||||
"generatingMedia": "Generating Media",
|
||||
"doNotCloseWindow": "Please do not close this window while generation is in progress.",
|
||||
"generationComplete": "Generation Complete",
|
||||
"generationFailed": "Generation Failed",
|
||||
"mediaAddedToPrompt": "Media has been added to your prompt.",
|
||||
"mediaGenerated": "Media generated successfully!",
|
||||
"orGenerateMedia": "Or generate with AI",
|
||||
"close": "Close",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Connecting to server...",
|
||||
"connected": "Connected, waiting in queue...",
|
||||
"queued": "Waiting in queue...",
|
||||
"accepted": "Task accepted...",
|
||||
"preprocessStart": "Preprocessing...",
|
||||
"preprocessEnd": "Preprocessing complete",
|
||||
"gpuAssigned": "GPU assigned, waiting in queue...",
|
||||
"started": "Generation started...",
|
||||
"generating": "Generating...",
|
||||
"processingOutput": "Processing output...",
|
||||
"ending": "Generation complete",
|
||||
"postprocessStart": "Post-processing...",
|
||||
"postprocessEnd": "Finalizing...",
|
||||
"complete": "Complete!",
|
||||
"error": "Error occurred",
|
||||
"errorProcessing": "Error processing..."
|
||||
},
|
||||
"privateDescription": "Private prompts are only visible to you",
|
||||
"requiresMediaUpload": "Requires Media Upload",
|
||||
"attachedMediaType": "Attached Media Type",
|
||||
|
||||
@@ -267,6 +267,43 @@
|
||||
"videoTooLarge": "Video demasiado grande. El tamaño máximo es 4MB.",
|
||||
"invalidFileType": "Tipo de archivo inválido. Solo se permiten JPEG, PNG, GIF y WebP.",
|
||||
"invalidVideoType": "Tipo de video inválido. Solo se permiten videos MP4.",
|
||||
"uploadMedia": "Subir Multimedia",
|
||||
"generateMedia": "Generar",
|
||||
"chooseGenerator": "Elegir Generador",
|
||||
"uploadInstead": "Subir en su lugar",
|
||||
"confirmGeneration": "Confirmar Generación",
|
||||
"confirmGenerationDescription": "¿Generar multimedia con {provider} usando el modelo {model}?",
|
||||
"promptPreview": "Vista previa del prompt",
|
||||
"noPromptProvided": "No se proporcionó prompt",
|
||||
"inputImage": "Imagen de entrada",
|
||||
"aspectRatio": "Relación de Aspecto",
|
||||
"startGeneration": "Iniciar Generación",
|
||||
"generatingMedia": "Generando Multimedia",
|
||||
"doNotCloseWindow": "Por favor no cierre esta ventana mientras se genera.",
|
||||
"generationComplete": "Generación Completada",
|
||||
"generationFailed": "Generación Fallida",
|
||||
"mediaAddedToPrompt": "El multimedia se ha añadido a tu prompt.",
|
||||
"mediaGenerated": "¡Multimedia generado exitosamente!",
|
||||
"orGenerateMedia": "O generar con IA",
|
||||
"close": "Cerrar",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Conectando al servidor...",
|
||||
"connected": "Conectado, esperando en cola...",
|
||||
"queued": "Esperando en cola...",
|
||||
"accepted": "Tarea aceptada...",
|
||||
"preprocessStart": "Preprocesando...",
|
||||
"preprocessEnd": "Preprocesamiento completado",
|
||||
"gpuAssigned": "GPU asignada, esperando en cola...",
|
||||
"started": "Generación iniciada...",
|
||||
"generating": "Generando...",
|
||||
"processingOutput": "Procesando salida...",
|
||||
"ending": "Generación completada",
|
||||
"postprocessStart": "Postprocesando...",
|
||||
"postprocessEnd": "Finalizando...",
|
||||
"complete": "¡Completado!",
|
||||
"error": "Ocurrió un error",
|
||||
"errorProcessing": "Error de procesamiento..."
|
||||
},
|
||||
"privateDescription": "Los prompts privados solo son visibles para ti",
|
||||
"update": "Actualizar",
|
||||
"createButton": "Crear",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "Vidéo trop volumineuse. La taille maximale est de 4Mo.",
|
||||
"invalidFileType": "Type de fichier invalide. Seuls JPEG, PNG, GIF et WebP sont autorisés.",
|
||||
"invalidVideoType": "Type de vidéo invalide. Seules les vidéos MP4 sont autorisées.",
|
||||
"uploadMedia": "Télécharger un média",
|
||||
"generateMedia": "Générer",
|
||||
"chooseGenerator": "Choisir un générateur",
|
||||
"uploadInstead": "Télécharger à la place",
|
||||
"confirmGeneration": "Confirmer la génération",
|
||||
"confirmGenerationDescription": "Générer un média avec {provider} en utilisant le modèle {model} ?",
|
||||
"promptPreview": "Aperçu du prompt",
|
||||
"noPromptProvided": "Aucun prompt fourni",
|
||||
"inputImage": "Image d'entrée",
|
||||
"aspectRatio": "Ratio d'aspect",
|
||||
"startGeneration": "Démarrer la génération",
|
||||
"generatingMedia": "Génération du média",
|
||||
"doNotCloseWindow": "Veuillez ne pas fermer cette fenêtre pendant la génération.",
|
||||
"generationComplete": "Génération terminée",
|
||||
"generationFailed": "Échec de la génération",
|
||||
"mediaAddedToPrompt": "Le média a été ajouté à votre prompt.",
|
||||
"mediaGenerated": "Média généré avec succès !",
|
||||
"orGenerateMedia": "Ou générer avec l'IA",
|
||||
"close": "Fermer",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Connexion au serveur...",
|
||||
"connected": "Connecté, en attente dans la file...",
|
||||
"queued": "En attente dans la file...",
|
||||
"accepted": "Tâche acceptée...",
|
||||
"preprocessStart": "Prétraitement...",
|
||||
"preprocessEnd": "Prétraitement terminé",
|
||||
"gpuAssigned": "GPU attribué, en attente dans la file...",
|
||||
"started": "Génération démarrée...",
|
||||
"generating": "Génération en cours...",
|
||||
"processingOutput": "Traitement de la sortie...",
|
||||
"ending": "Génération terminée",
|
||||
"postprocessStart": "Post-traitement...",
|
||||
"postprocessEnd": "Finalisation...",
|
||||
"complete": "Terminé !",
|
||||
"error": "Une erreur s'est produite",
|
||||
"errorProcessing": "Erreur de traitement..."
|
||||
},
|
||||
"privateDescription": "Les prompts privés ne sont visibles que par vous",
|
||||
"requiresMediaUpload": "Nécessite un Upload de Média",
|
||||
"attachedMediaType": "Type de Média Joint",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "הסרטון גדול מדי. גודל מקסימלי הוא 4MB.",
|
||||
"invalidFileType": "סוג קובץ לא חוקי. רק JPEG, PNG, GIF ו-WebP מותרים.",
|
||||
"invalidVideoType": "סוג סרטון לא חוקי. רק סרטוני MP4 מותרים.",
|
||||
"uploadMedia": "העלאת מדיה",
|
||||
"generateMedia": "יצירה",
|
||||
"chooseGenerator": "בחר מחולל",
|
||||
"uploadInstead": "העלאה במקום זאת",
|
||||
"confirmGeneration": "אישור יצירה",
|
||||
"confirmGenerationDescription": "ליצור מדיה עם {provider} באמצעות מודל {model}?",
|
||||
"promptPreview": "תצוגה מקדימה של פרומפט",
|
||||
"noPromptProvided": "לא סופק פרומפט",
|
||||
"inputImage": "תמונת קלט",
|
||||
"aspectRatio": "יחס גובה-רוחב",
|
||||
"startGeneration": "התחל יצירה",
|
||||
"generatingMedia": "יוצר מדיה",
|
||||
"doNotCloseWindow": "אנא אל תסגור חלון זה בזמן היצירה.",
|
||||
"generationComplete": "היצירה הושלמה",
|
||||
"generationFailed": "היצירה נכשלה",
|
||||
"mediaAddedToPrompt": "המדיה נוספה לפרומפט שלך.",
|
||||
"mediaGenerated": "המדיה נוצרה בהצלחה!",
|
||||
"orGenerateMedia": "או יצירה עם AI",
|
||||
"close": "סגור",
|
||||
"mediaGeneration": {
|
||||
"connecting": "מתחבר לשרת...",
|
||||
"connected": "מחובר, ממתין בתור...",
|
||||
"queued": "ממתין בתור...",
|
||||
"accepted": "המשימה התקבלה...",
|
||||
"preprocessStart": "עיבוד מקדים...",
|
||||
"preprocessEnd": "העיבוד המקדים הושלם",
|
||||
"gpuAssigned": "GPU הוקצה, ממתין בתור...",
|
||||
"started": "היצירה החלה...",
|
||||
"generating": "יוצר...",
|
||||
"processingOutput": "מעבד פלט...",
|
||||
"ending": "היצירה הושלמה",
|
||||
"postprocessStart": "עיבוד לאחר...",
|
||||
"postprocessEnd": "מסיים...",
|
||||
"complete": "הושלם!",
|
||||
"error": "אירעה שגיאה",
|
||||
"errorProcessing": "שגיאת עיבוד..."
|
||||
},
|
||||
"privateDescription": "פרומפטים פרטיים גלויים רק לך",
|
||||
"requiresMediaUpload": "דורש העלאת מדיה",
|
||||
"attachedMediaType": "סוג מדיה מצורף",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "Video troppo grande. La dimensione massima è 4MB.",
|
||||
"invalidFileType": "Tipo di file non valido. Sono consentiti solo JPEG, PNG, GIF e WebP.",
|
||||
"invalidVideoType": "Tipo di video non valido. Sono consentiti solo video MP4.",
|
||||
"uploadMedia": "Carica Media",
|
||||
"generateMedia": "Genera",
|
||||
"chooseGenerator": "Scegli Generatore",
|
||||
"uploadInstead": "Carica invece",
|
||||
"confirmGeneration": "Conferma Generazione",
|
||||
"confirmGenerationDescription": "Generare media con {provider} usando il modello {model}?",
|
||||
"promptPreview": "Anteprima prompt",
|
||||
"noPromptProvided": "Nessun prompt fornito",
|
||||
"inputImage": "Immagine di input",
|
||||
"aspectRatio": "Proporzioni",
|
||||
"startGeneration": "Avvia Generazione",
|
||||
"generatingMedia": "Generazione Media",
|
||||
"doNotCloseWindow": "Per favore non chiudere questa finestra durante la generazione.",
|
||||
"generationComplete": "Generazione Completata",
|
||||
"generationFailed": "Generazione Fallita",
|
||||
"mediaAddedToPrompt": "Il media è stato aggiunto al tuo prompt.",
|
||||
"mediaGenerated": "Media generato con successo!",
|
||||
"orGenerateMedia": "O genera con IA",
|
||||
"close": "Chiudi",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Connessione al server...",
|
||||
"connected": "Connesso, in attesa nella coda...",
|
||||
"queued": "In attesa nella coda...",
|
||||
"accepted": "Attività accettata...",
|
||||
"preprocessStart": "Pre-elaborazione...",
|
||||
"preprocessEnd": "Pre-elaborazione completata",
|
||||
"gpuAssigned": "GPU assegnata, in attesa nella coda...",
|
||||
"started": "Generazione avviata...",
|
||||
"generating": "Generazione in corso...",
|
||||
"processingOutput": "Elaborazione output...",
|
||||
"ending": "Generazione completata",
|
||||
"postprocessStart": "Post-elaborazione...",
|
||||
"postprocessEnd": "Finalizzazione...",
|
||||
"complete": "Completato!",
|
||||
"error": "Si è verificato un errore",
|
||||
"errorProcessing": "Errore di elaborazione..."
|
||||
},
|
||||
"privateDescription": "I prompt privati sono visibili solo a te",
|
||||
"requiresMediaUpload": "Richiede Caricamento Media",
|
||||
"attachedMediaType": "Tipo di Media Allegato",
|
||||
|
||||
@@ -267,6 +267,43 @@
|
||||
"videoTooLarge": "動画が大きすぎます。最大サイズは4MBです。",
|
||||
"invalidFileType": "無効なファイル形式です。JPEG、PNG、GIF、WebPのみ許可されています。",
|
||||
"invalidVideoType": "無効な動画形式です。MP4動画のみ許可されています。",
|
||||
"uploadMedia": "メディアをアップロード",
|
||||
"generateMedia": "生成",
|
||||
"chooseGenerator": "生成ツールを選択",
|
||||
"uploadInstead": "代わりにアップロード",
|
||||
"confirmGeneration": "生成を確認",
|
||||
"confirmGenerationDescription": "{provider}で{model}モデルを使用してメディアを生成しますか?",
|
||||
"promptPreview": "プロンプトプレビュー",
|
||||
"noPromptProvided": "プロンプトが入力されていません",
|
||||
"inputImage": "入力画像",
|
||||
"aspectRatio": "アスペクト比",
|
||||
"startGeneration": "生成を開始",
|
||||
"generatingMedia": "メディア生成中",
|
||||
"doNotCloseWindow": "生成中はこのウィンドウを閉じないでください。",
|
||||
"generationComplete": "生成完了",
|
||||
"generationFailed": "生成失敗",
|
||||
"mediaAddedToPrompt": "メディアがプロンプトに追加されました。",
|
||||
"mediaGenerated": "メディアが正常に生成されました!",
|
||||
"orGenerateMedia": "またはAIで生成",
|
||||
"close": "閉じる",
|
||||
"mediaGeneration": {
|
||||
"connecting": "サーバーに接続中...",
|
||||
"connected": "接続完了、キューで待機中...",
|
||||
"queued": "キューで待機中...",
|
||||
"accepted": "タスクが受理されました...",
|
||||
"preprocessStart": "前処理中...",
|
||||
"preprocessEnd": "前処理完了",
|
||||
"gpuAssigned": "GPUが割り当てられました、キューで待機中...",
|
||||
"started": "生成開始...",
|
||||
"generating": "生成中...",
|
||||
"processingOutput": "出力を処理中...",
|
||||
"ending": "生成完了",
|
||||
"postprocessStart": "後処理中...",
|
||||
"postprocessEnd": "最終処理中...",
|
||||
"complete": "完了!",
|
||||
"error": "エラーが発生しました",
|
||||
"errorProcessing": "処理エラー..."
|
||||
},
|
||||
"privateDescription": "非公開プロンプトはあなただけに表示されます",
|
||||
"update": "更新",
|
||||
"createButton": "作成",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "동영상이 너무 큽니다. 최대 크기는 4MB입니다.",
|
||||
"invalidFileType": "잘못된 파일 형식입니다. JPEG, PNG, GIF, WebP만 허용됩니다.",
|
||||
"invalidVideoType": "잘못된 동영상 형식입니다. MP4 동영상만 허용됩니다.",
|
||||
"uploadMedia": "미디어 업로드",
|
||||
"generateMedia": "생성",
|
||||
"chooseGenerator": "생성기 선택",
|
||||
"uploadInstead": "대신 업로드",
|
||||
"confirmGeneration": "생성 확인",
|
||||
"confirmGenerationDescription": "{provider}에서 {model} 모델을 사용하여 미디어를 생성하시겠습니까?",
|
||||
"promptPreview": "프롬프트 미리보기",
|
||||
"noPromptProvided": "프롬프트가 제공되지 않음",
|
||||
"inputImage": "입력 이미지",
|
||||
"aspectRatio": "화면 비율",
|
||||
"startGeneration": "생성 시작",
|
||||
"generatingMedia": "미디어 생성 중",
|
||||
"doNotCloseWindow": "생성 중에는 이 창을 닫지 마세요.",
|
||||
"generationComplete": "생성 완료",
|
||||
"generationFailed": "생성 실패",
|
||||
"mediaAddedToPrompt": "미디어가 프롬프트에 추가되었습니다.",
|
||||
"mediaGenerated": "미디어가 성공적으로 생성되었습니다!",
|
||||
"orGenerateMedia": "또는 AI로 생성",
|
||||
"close": "닫기",
|
||||
"mediaGeneration": {
|
||||
"connecting": "서버에 연결 중...",
|
||||
"connected": "연결됨, 대기열에서 대기 중...",
|
||||
"queued": "대기열에서 대기 중...",
|
||||
"accepted": "작업이 수락되었습니다...",
|
||||
"preprocessStart": "전처리 중...",
|
||||
"preprocessEnd": "전처리 완료",
|
||||
"gpuAssigned": "GPU 할당됨, 대기열에서 대기 중...",
|
||||
"started": "생성 시작...",
|
||||
"generating": "생성 중...",
|
||||
"processingOutput": "출력 처리 중...",
|
||||
"ending": "생성 완료",
|
||||
"postprocessStart": "후처리 중...",
|
||||
"postprocessEnd": "마무리 중...",
|
||||
"complete": "완료!",
|
||||
"error": "오류가 발생했습니다",
|
||||
"errorProcessing": "처리 오류..."
|
||||
},
|
||||
"privateDescription": "비공개 프롬프트는 본인만 볼 수 있습니다",
|
||||
"requiresMediaUpload": "미디어 업로드 필요",
|
||||
"attachedMediaType": "첨부 미디어 유형",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "Vídeo muito grande. O tamanho máximo é 4MB.",
|
||||
"invalidFileType": "Tipo de arquivo inválido. Apenas JPEG, PNG, GIF e WebP são permitidos.",
|
||||
"invalidVideoType": "Tipo de vídeo inválido. Apenas vídeos MP4 são permitidos.",
|
||||
"uploadMedia": "Enviar Mídia",
|
||||
"generateMedia": "Gerar",
|
||||
"chooseGenerator": "Escolher Gerador",
|
||||
"uploadInstead": "Enviar em vez disso",
|
||||
"confirmGeneration": "Confirmar Geração",
|
||||
"confirmGenerationDescription": "Gerar mídia com {provider} usando o modelo {model}?",
|
||||
"promptPreview": "Prévia do prompt",
|
||||
"noPromptProvided": "Nenhum prompt fornecido",
|
||||
"inputImage": "Imagem de entrada",
|
||||
"aspectRatio": "Proporção",
|
||||
"startGeneration": "Iniciar Geração",
|
||||
"generatingMedia": "Gerando Mídia",
|
||||
"doNotCloseWindow": "Por favor não feche esta janela durante a geração.",
|
||||
"generationComplete": "Geração Concluída",
|
||||
"generationFailed": "Geração Falhou",
|
||||
"mediaAddedToPrompt": "A mídia foi adicionada ao seu prompt.",
|
||||
"mediaGenerated": "Mídia gerada com sucesso!",
|
||||
"orGenerateMedia": "Ou gerar com IA",
|
||||
"close": "Fechar",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Conectando ao servidor...",
|
||||
"connected": "Conectado, aguardando na fila...",
|
||||
"queued": "Aguardando na fila...",
|
||||
"accepted": "Tarefa aceita...",
|
||||
"preprocessStart": "Pré-processando...",
|
||||
"preprocessEnd": "Pré-processamento concluído",
|
||||
"gpuAssigned": "GPU atribuída, aguardando na fila...",
|
||||
"started": "Geração iniciada...",
|
||||
"generating": "Gerando...",
|
||||
"processingOutput": "Processando saída...",
|
||||
"ending": "Geração concluída",
|
||||
"postprocessStart": "Pós-processando...",
|
||||
"postprocessEnd": "Finalizando...",
|
||||
"complete": "Concluído!",
|
||||
"error": "Ocorreu um erro",
|
||||
"errorProcessing": "Erro de processamento..."
|
||||
},
|
||||
"privateDescription": "Prompts privados são visíveis apenas para você",
|
||||
"requiresMediaUpload": "Requer Upload de Mídia",
|
||||
"attachedMediaType": "Tipo de Mídia Anexada",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "Видео слишком большое. Максимальный размер 4MB.",
|
||||
"invalidFileType": "Недопустимый тип файла. Разрешены только JPEG, PNG, GIF и WebP.",
|
||||
"invalidVideoType": "Недопустимый тип видео. Разрешены только видео MP4.",
|
||||
"uploadMedia": "Загрузить медиа",
|
||||
"generateMedia": "Сгенерировать",
|
||||
"chooseGenerator": "Выбрать генератор",
|
||||
"uploadInstead": "Загрузить вместо этого",
|
||||
"confirmGeneration": "Подтвердить генерацию",
|
||||
"confirmGenerationDescription": "Сгенерировать медиа с помощью {provider} используя модель {model}?",
|
||||
"promptPreview": "Предпросмотр промпта",
|
||||
"noPromptProvided": "Промпт не указан",
|
||||
"inputImage": "Входное изображение",
|
||||
"aspectRatio": "Соотношение сторон",
|
||||
"startGeneration": "Начать генерацию",
|
||||
"generatingMedia": "Генерация медиа",
|
||||
"doNotCloseWindow": "Пожалуйста, не закрывайте это окно во время генерации.",
|
||||
"generationComplete": "Генерация завершена",
|
||||
"generationFailed": "Генерация не удалась",
|
||||
"mediaAddedToPrompt": "Медиа добавлено к вашему промпту.",
|
||||
"mediaGenerated": "Медиа успешно сгенерировано!",
|
||||
"orGenerateMedia": "Или сгенерировать с ИИ",
|
||||
"close": "Закрыть",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Подключение к серверу...",
|
||||
"connected": "Подключено, ожидание в очереди...",
|
||||
"queued": "Ожидание в очереди...",
|
||||
"accepted": "Задача принята...",
|
||||
"preprocessStart": "Предобработка...",
|
||||
"preprocessEnd": "Предобработка завершена",
|
||||
"gpuAssigned": "GPU назначен, ожидание в очереди...",
|
||||
"started": "Генерация начата...",
|
||||
"generating": "Генерация...",
|
||||
"processingOutput": "Обработка вывода...",
|
||||
"ending": "Генерация завершена",
|
||||
"postprocessStart": "Постобработка...",
|
||||
"postprocessEnd": "Завершение...",
|
||||
"complete": "Готово!",
|
||||
"error": "Произошла ошибка",
|
||||
"errorProcessing": "Ошибка обработки..."
|
||||
},
|
||||
"privateDescription": "Приватные промпты видны только вам",
|
||||
"requiresMediaUpload": "Требуется загрузка медиа",
|
||||
"attachedMediaType": "Тип прикрепленного медиа",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "Video çok büyük. Maksimum boyut 4MB'dir.",
|
||||
"invalidFileType": "Geçersiz dosya türü. Yalnızca JPEG, PNG, GIF ve WebP'ye izin verilir.",
|
||||
"invalidVideoType": "Geçersiz video türü. Yalnızca MP4 videolara izin verilir.",
|
||||
"uploadMedia": "Medya Yükle",
|
||||
"generateMedia": "Oluştur",
|
||||
"chooseGenerator": "Oluşturucu Seç",
|
||||
"uploadInstead": "Bunun Yerine Yükle",
|
||||
"confirmGeneration": "Oluşturmayı Onayla",
|
||||
"confirmGenerationDescription": "{provider} ile {model} modeli kullanarak medya oluşturulsun mu?",
|
||||
"promptPreview": "Prompt önizlemesi",
|
||||
"noPromptProvided": "Prompt sağlanmadı",
|
||||
"inputImage": "Girdi görseli",
|
||||
"aspectRatio": "En-Boy Oranı",
|
||||
"startGeneration": "Oluşturmayı Başlat",
|
||||
"generatingMedia": "Medya Oluşturuluyor",
|
||||
"doNotCloseWindow": "Oluşturma devam ederken lütfen bu pencereyi kapatmayın.",
|
||||
"generationComplete": "Oluşturma Tamamlandı",
|
||||
"generationFailed": "Oluşturma Başarısız",
|
||||
"mediaAddedToPrompt": "Medya promptunuza eklendi.",
|
||||
"mediaGenerated": "Medya başarıyla oluşturuldu!",
|
||||
"orGenerateMedia": "Veya AI ile oluştur",
|
||||
"close": "Kapat",
|
||||
"mediaGeneration": {
|
||||
"connecting": "Sunucuya bağlanılıyor...",
|
||||
"connected": "Bağlandı, kuyrukta bekleniyor...",
|
||||
"queued": "Kuyrukta bekleniyor...",
|
||||
"accepted": "Görev kabul edildi...",
|
||||
"preprocessStart": "Ön işleme başlıyor...",
|
||||
"preprocessEnd": "Ön işleme tamamlandı",
|
||||
"gpuAssigned": "GPU atandı, kuyrukta bekleniyor...",
|
||||
"started": "Oluşturma başladı...",
|
||||
"generating": "Oluşturuluyor...",
|
||||
"processingOutput": "Çıktı işleniyor...",
|
||||
"ending": "Oluşturma tamamlandı",
|
||||
"postprocessStart": "Son işleme başlıyor...",
|
||||
"postprocessEnd": "Sonlandırılıyor...",
|
||||
"complete": "Tamamlandı!",
|
||||
"error": "Hata oluştu",
|
||||
"errorProcessing": "İşleme hatası..."
|
||||
},
|
||||
"privateDescription": "Özel promptlar yalnızca size görünür",
|
||||
"requiresMediaUpload": "Medya Yüklemesi Gerekli",
|
||||
"attachedMediaType": "Ekli Medya Türü",
|
||||
|
||||
@@ -259,6 +259,43 @@
|
||||
"videoTooLarge": "视频太大。最大大小为4MB。",
|
||||
"invalidFileType": "无效的文件类型。仅允许JPEG、PNG、GIF和WebP。",
|
||||
"invalidVideoType": "无效的视频类型。仅允许MP4视频。",
|
||||
"uploadMedia": "上传媒体",
|
||||
"generateMedia": "生成",
|
||||
"chooseGenerator": "选择生成器",
|
||||
"uploadInstead": "改为上传",
|
||||
"confirmGeneration": "确认生成",
|
||||
"confirmGenerationDescription": "使用 {provider} 的 {model} 模型生成媒体?",
|
||||
"promptPreview": "提示词预览",
|
||||
"noPromptProvided": "未提供提示词",
|
||||
"inputImage": "输入图片",
|
||||
"aspectRatio": "宽高比",
|
||||
"startGeneration": "开始生成",
|
||||
"generatingMedia": "正在生成媒体",
|
||||
"doNotCloseWindow": "生成过程中请勿关闭此窗口。",
|
||||
"generationComplete": "生成完成",
|
||||
"generationFailed": "生成失败",
|
||||
"mediaAddedToPrompt": "媒体已添加到您的提示词。",
|
||||
"mediaGenerated": "媒体生成成功!",
|
||||
"orGenerateMedia": "或使用AI生成",
|
||||
"close": "关闭",
|
||||
"mediaGeneration": {
|
||||
"connecting": "正在连接服务器...",
|
||||
"connected": "已连接,正在排队等待...",
|
||||
"queued": "正在排队等待...",
|
||||
"accepted": "任务已接受...",
|
||||
"preprocessStart": "正在预处理...",
|
||||
"preprocessEnd": "预处理完成",
|
||||
"gpuAssigned": "GPU已分配,正在排队等待...",
|
||||
"started": "生成已开始...",
|
||||
"generating": "正在生成...",
|
||||
"processingOutput": "正在处理输出...",
|
||||
"ending": "生成完成",
|
||||
"postprocessStart": "正在后处理...",
|
||||
"postprocessEnd": "正在完成...",
|
||||
"complete": "完成!",
|
||||
"error": "发生错误",
|
||||
"errorProcessing": "处理错误..."
|
||||
},
|
||||
"privateDescription": "私有提示词仅对您可见",
|
||||
"requiresMediaUpload": "需要媒体上传",
|
||||
"attachedMediaType": "附加媒体类型",
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import {
|
||||
initializeMediaGenerators,
|
||||
getMediaGeneratorPlugin,
|
||||
getAvailableModels,
|
||||
isMediaGenerationAvailable,
|
||||
} from "@/lib/plugins/media-generators";
|
||||
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
initializeMediaGenerators();
|
||||
|
||||
const available = isMediaGenerationAvailable();
|
||||
const imageModels = getAvailableModels("image");
|
||||
const videoModels = getAvailableModels("video");
|
||||
|
||||
return NextResponse.json({
|
||||
available,
|
||||
imageModels,
|
||||
videoModels,
|
||||
});
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { prompt, model, provider, type, inputImageUrl, resolution, aspectRatio } = body;
|
||||
|
||||
if (!prompt || !model || !provider || !type) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields: prompt, model, provider, type" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
initializeMediaGenerators();
|
||||
|
||||
const plugin = getMediaGeneratorPlugin(provider);
|
||||
|
||||
if (!plugin) {
|
||||
return NextResponse.json(
|
||||
{ error: `Provider "${provider}" not found` },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!plugin.isEnabled()) {
|
||||
return NextResponse.json(
|
||||
{ error: `Provider "${provider}" is not enabled` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const task = await plugin.startGeneration({
|
||||
prompt,
|
||||
model,
|
||||
type,
|
||||
inputImageUrl,
|
||||
resolution,
|
||||
aspectRatio,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
taskId: task.taskId,
|
||||
socketAccessToken: task.socketAccessToken,
|
||||
webSocketUrl: plugin.getWebSocketUrl(),
|
||||
provider,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Media generation error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: error instanceof Error ? error.message : "Generation failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,451 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Loader2, Wand2, Upload, ChevronDown, AlertCircle, CheckCircle2 } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { toast } from "sonner";
|
||||
import type { WebSocketHandler, WebSocketCallbacks, GenerationStatusKey, AspectRatio } from "@/lib/plugins/media-generators/types";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const ASPECT_RATIOS: { value: AspectRatio; label: string }[] = [
|
||||
{ value: "1:1", label: "1:1 (Square)" },
|
||||
{ value: "16:9", label: "16:9 (Landscape)" },
|
||||
{ value: "9:16", label: "9:16 (Portrait)" },
|
||||
{ value: "4:3", label: "4:3 (Standard)" },
|
||||
{ value: "3:4", label: "3:4 (Portrait)" },
|
||||
{ value: "3:2", label: "3:2 (Photo)" },
|
||||
{ value: "2:3", label: "2:3 (Portrait)" },
|
||||
];
|
||||
import { wiroGeneratorPlugin } from "@/lib/plugins/media-generators/wiro";
|
||||
import { falGeneratorPlugin } from "@/lib/plugins/media-generators/fal";
|
||||
|
||||
interface MediaGeneratorModel {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "image" | "video";
|
||||
provider: string;
|
||||
}
|
||||
|
||||
interface MediaGeneratorProps {
|
||||
prompt: string;
|
||||
mediaType: "IMAGE" | "VIDEO";
|
||||
onMediaGenerated: (url: string) => void;
|
||||
onUploadClick: () => void;
|
||||
inputImageUrl?: string;
|
||||
}
|
||||
|
||||
type GenerationStatus =
|
||||
| "idle"
|
||||
| "confirming"
|
||||
| "starting"
|
||||
| "queued"
|
||||
| "processing"
|
||||
| "completed"
|
||||
| "error";
|
||||
|
||||
// Get WebSocket handler for a provider
|
||||
function getProviderHandler(provider: string): WebSocketHandler {
|
||||
switch (provider) {
|
||||
case "wiro":
|
||||
return wiroGeneratorPlugin.webSocketHandler;
|
||||
case "fal":
|
||||
return falGeneratorPlugin.webSocketHandler;
|
||||
default:
|
||||
throw new Error(`Unknown provider: ${provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function MediaGenerator({
|
||||
prompt,
|
||||
mediaType,
|
||||
onMediaGenerated,
|
||||
onUploadClick,
|
||||
inputImageUrl,
|
||||
}: MediaGeneratorProps) {
|
||||
const t = useTranslations("prompts");
|
||||
const [models, setModels] = useState<MediaGeneratorModel[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedModel, setSelectedModel] = useState<MediaGeneratorModel | null>(null);
|
||||
const [aspectRatio, setAspectRatio] = useState<AspectRatio>("1:1");
|
||||
const [status, setStatus] = useState<GenerationStatus>("idle");
|
||||
const [statusKey, setStatusKey] = useState<GenerationStatusKey | null>(null);
|
||||
const [progress, setProgress] = useState(0);
|
||||
|
||||
// Get translated status message
|
||||
const statusMessage = statusKey ? t(`mediaGeneration.${statusKey}`) : "";
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
// Fetch available models
|
||||
useEffect(() => {
|
||||
async function fetchModels() {
|
||||
try {
|
||||
const response = await fetch("/api/media-generate");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
const relevantModels = mediaType === "IMAGE" ? data.imageModels : data.videoModels;
|
||||
setModels(relevantModels || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch media generation models:", err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchModels();
|
||||
}, [mediaType]);
|
||||
|
||||
const cleanupWebSocket = useCallback(() => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
wsRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanupWebSocket();
|
||||
};
|
||||
}, [cleanupWebSocket]);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!selectedModel) return;
|
||||
|
||||
setStatus("starting");
|
||||
setError(null);
|
||||
setProgress(10);
|
||||
setStatusKey("connecting");
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/media-generate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
prompt,
|
||||
model: selectedModel.id,
|
||||
provider: selectedModel.provider,
|
||||
type: selectedModel.type,
|
||||
inputImageUrl,
|
||||
resolution: "1K",
|
||||
aspectRatio,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data.error || "Failed to start generation");
|
||||
}
|
||||
|
||||
const { socketAccessToken, webSocketUrl, provider } = await response.json();
|
||||
|
||||
// Get provider-specific handler
|
||||
const handler = getProviderHandler(provider);
|
||||
|
||||
// Connect to WebSocket for progress tracking
|
||||
setStatus("queued");
|
||||
setProgress(20);
|
||||
setStatusKey("connecting");
|
||||
|
||||
const ws = new WebSocket(webSocketUrl);
|
||||
wsRef.current = ws;
|
||||
|
||||
// Create callbacks for the handler
|
||||
const callbacks: WebSocketCallbacks = {
|
||||
setProgress,
|
||||
setStatus,
|
||||
setStatusMessage: setStatusKey,
|
||||
setError,
|
||||
onComplete: (urls: string[]) => {
|
||||
if (urls.length > 0) {
|
||||
onMediaGenerated(urls[0]);
|
||||
toast.success(t("mediaGenerated"));
|
||||
}
|
||||
// Reset after a delay
|
||||
setTimeout(() => {
|
||||
setStatus("idle");
|
||||
setProgress(0);
|
||||
setStatusKey(null);
|
||||
setSelectedModel(null);
|
||||
}, 2000);
|
||||
},
|
||||
onCleanup: cleanupWebSocket,
|
||||
};
|
||||
|
||||
ws.onopen = () => {
|
||||
const initMessage = handler.getInitMessage(socketAccessToken);
|
||||
if (initMessage) {
|
||||
ws.send(initMessage);
|
||||
}
|
||||
setStatusKey("connected");
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
handler.handleMessage(event, callbacks);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
setStatus("error");
|
||||
setError("WebSocket connection error");
|
||||
cleanupWebSocket();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (status !== "completed" && status !== "error" && status !== "idle") {
|
||||
// Unexpected close
|
||||
setStatus("error");
|
||||
setError("Connection closed unexpectedly");
|
||||
}
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
setStatus("error");
|
||||
setError(err instanceof Error ? err.message : "Generation failed");
|
||||
cleanupWebSocket();
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmGenerate = () => {
|
||||
setStatus("confirming");
|
||||
};
|
||||
|
||||
const handleCancelGeneration = () => {
|
||||
setStatus("idle");
|
||||
setSelectedModel(null);
|
||||
setError(null);
|
||||
setProgress(0);
|
||||
setStatusKey(null);
|
||||
cleanupWebSocket();
|
||||
};
|
||||
|
||||
const isGenerating = status === "starting" || status === "queued" || status === "processing";
|
||||
|
||||
// No models available - just show upload
|
||||
if (!isLoading && models.length === 0) {
|
||||
return (
|
||||
<Button type="button" variant="outline" size="sm" onClick={onUploadClick}>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{t("uploadMedia")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Group models by provider
|
||||
const modelsByProvider = models.reduce((acc, model) => {
|
||||
if (!acc[model.provider]) {
|
||||
acc[model.provider] = [];
|
||||
}
|
||||
acc[model.provider].push(model);
|
||||
return acc;
|
||||
}, {} as Record<string, MediaGeneratorModel[]>);
|
||||
|
||||
const providerNames: Record<string, string> = {
|
||||
wiro: "Wiro.ai",
|
||||
fal: "Fal.ai",
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="button" variant="outline" size="sm" disabled={isGenerating || isLoading}>
|
||||
{isLoading ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Wand2 className="h-4 w-4 mr-2" />
|
||||
{t("generateMedia")}
|
||||
<ChevronDown className="h-3 w-3 ml-2" />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-56">
|
||||
<DropdownMenuLabel>{t("chooseGenerator")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
{Object.entries(modelsByProvider).map(([provider, providerModels]) => (
|
||||
<div key={provider}>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
{providerNames[provider] || provider}
|
||||
</DropdownMenuLabel>
|
||||
{providerModels.map((model) => (
|
||||
<DropdownMenuItem
|
||||
key={`${provider}-${model.id}`}
|
||||
onClick={() => {
|
||||
setSelectedModel(model);
|
||||
handleConfirmGenerate();
|
||||
}}
|
||||
>
|
||||
<Wand2 className="h-4 w-4 mr-2" />
|
||||
{model.name}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onUploadClick}>
|
||||
<Upload className="h-4 w-4 mr-2" />
|
||||
{t("uploadInstead")}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{isGenerating && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
<span>{statusMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
<Dialog open={status === "confirming"} onOpenChange={(open) => !open && handleCancelGeneration()}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("confirmGeneration")}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("confirmGenerationDescription", {
|
||||
provider: providerNames[selectedModel?.provider || ""] || selectedModel?.provider || "",
|
||||
model: selectedModel?.name || ""
|
||||
})}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 py-4">
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">{t("promptPreview")}:</span>
|
||||
<p className="mt-1 p-2 rounded bg-muted text-muted-foreground text-xs line-clamp-3">
|
||||
{prompt || t("noPromptProvided")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{inputImageUrl && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">{t("inputImage")}:</span>
|
||||
<img
|
||||
src={inputImageUrl}
|
||||
alt="Input"
|
||||
className="mt-1 max-h-20 rounded border"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Aspect Ratio selector for image and video generation */}
|
||||
{selectedModel && (
|
||||
<div className="text-sm">
|
||||
<span className="font-medium">{t("aspectRatio")}:</span>
|
||||
<Select value={aspectRatio} onValueChange={(v) => setAspectRatio(v as AspectRatio)}>
|
||||
<SelectTrigger className="mt-1">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ASPECT_RATIOS.map((ar) => (
|
||||
<SelectItem key={ar.value} value={ar.value}>
|
||||
{ar.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={handleCancelGeneration}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="button" onClick={handleGenerate} disabled={!prompt}>
|
||||
<Wand2 className="h-4 w-4 mr-2" />
|
||||
{t("startGeneration")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Progress Dialog */}
|
||||
<Dialog open={isGenerating} onOpenChange={() => {}}>
|
||||
<DialogContent className="sm:max-w-md" onPointerDownOutside={(e) => e.preventDefault()}>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
{t("generatingMedia")}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t("doNotCloseWindow")}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
<Progress value={progress} className="h-2" />
|
||||
<p className="text-sm text-center text-muted-foreground">
|
||||
{statusMessage}
|
||||
</p>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Result/Error Dialog */}
|
||||
<Dialog open={status === "completed" || status === "error"} onOpenChange={handleCancelGeneration}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{status === "completed" ? (
|
||||
<>
|
||||
<CheckCircle2 className="h-5 w-5 text-green-500" />
|
||||
{t("generationComplete")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle className="h-5 w-5 text-destructive" />
|
||||
{t("generationFailed")}
|
||||
</>
|
||||
)}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{status === "error" && error && (
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
{status === "completed" && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("mediaAddedToPrompt")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="button" onClick={handleCancelGeneration}>
|
||||
{t("close")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { VariableWarning } from "./variable-warning";
|
||||
import { VariableHint } from "./variable-hint";
|
||||
import { ContributorSearch } from "./contributor-search";
|
||||
import { PromptBuilder, type PromptBuilderHandle } from "./prompt-builder";
|
||||
import { MediaGenerator } from "./media-generator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
@@ -44,15 +45,18 @@ interface MediaFieldProps {
|
||||
form: ReturnType<typeof useForm<PromptFormValues>>;
|
||||
t: (key: string) => string;
|
||||
promptType?: string;
|
||||
promptContent?: string;
|
||||
}
|
||||
|
||||
function MediaField({ form, t, promptType }: MediaFieldProps) {
|
||||
function MediaField({ form, t, promptType, promptContent }: MediaFieldProps) {
|
||||
const [storageMode, setStorageMode] = useState<string>("url");
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const mediaUrl = form.watch("mediaUrl");
|
||||
const isVideoType = promptType === "VIDEO";
|
||||
const mediaType = isVideoType ? "VIDEO" : "IMAGE";
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/config/storage")
|
||||
@@ -115,7 +119,20 @@ function MediaField({ form, t, promptType }: MediaFieldProps) {
|
||||
setUploadError(null);
|
||||
};
|
||||
|
||||
// URL mode: show text input
|
||||
const handleMediaGenerated = (url: string) => {
|
||||
form.setValue("mediaUrl", url);
|
||||
setShowUpload(false);
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
if (storageMode === "url") {
|
||||
setShowUpload(true);
|
||||
} else {
|
||||
fileInputRef.current?.click();
|
||||
}
|
||||
};
|
||||
|
||||
// URL mode with generator option
|
||||
if (storageMode === "url") {
|
||||
return (
|
||||
<FormField
|
||||
@@ -124,9 +141,42 @@ function MediaField({ form, t, promptType }: MediaFieldProps) {
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("mediaUrl")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("mediaUrlPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
<div className="space-y-3">
|
||||
{mediaUrl ? (
|
||||
<div className="space-y-2">
|
||||
<div className="relative inline-block">
|
||||
{isVideoType ? (
|
||||
<video src={mediaUrl} controls className="max-h-40 rounded-md border" />
|
||||
) : (
|
||||
<img src={mediaUrl} alt="Preview" className="max-h-40 rounded-md border" />
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="absolute -top-2 -right-2 h-6 w-6"
|
||||
onClick={clearMedia}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<MediaGenerator
|
||||
prompt={promptContent || ""}
|
||||
mediaType={mediaType as "IMAGE" | "VIDEO"}
|
||||
onMediaGenerated={handleMediaGenerated}
|
||||
onUploadClick={handleUploadClick}
|
||||
/>
|
||||
{showUpload && (
|
||||
<FormControl>
|
||||
<Input placeholder={t("mediaUrlPlaceholder")} {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
@@ -134,7 +184,7 @@ function MediaField({ form, t, promptType }: MediaFieldProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// Upload mode: show file upload
|
||||
// Upload mode: show file upload with generator option
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -143,7 +193,7 @@ function MediaField({ form, t, promptType }: MediaFieldProps) {
|
||||
<FormItem>
|
||||
<FormLabel>{t(isVideoType ? "mediaVideo" : "mediaImage")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{mediaUrl ? (
|
||||
<div className="relative inline-block">
|
||||
{isVideoType ? (
|
||||
@@ -170,22 +220,32 @@ function MediaField({ form, t, promptType }: MediaFieldProps) {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center gap-2 p-6 border-2 border-dashed rounded-md cursor-pointer hover:border-primary/50 transition-colors"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
{isUploading ? (
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<Upload className="h-8 w-8 text-muted-foreground" />
|
||||
<>
|
||||
<MediaGenerator
|
||||
prompt={promptContent || ""}
|
||||
mediaType={mediaType as "IMAGE" | "VIDEO"}
|
||||
onMediaGenerated={handleMediaGenerated}
|
||||
onUploadClick={handleUploadClick}
|
||||
/>
|
||||
{showUpload && (
|
||||
<div
|
||||
className="flex flex-col items-center justify-center gap-2 p-6 border-2 border-dashed rounded-md cursor-pointer hover:border-primary/50 transition-colors"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
>
|
||||
{isUploading ? (
|
||||
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||
) : (
|
||||
<Upload className="h-8 w-8 text-muted-foreground" />
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isUploading ? t("uploading") : t(isVideoType ? "clickToUploadVideo" : "clickToUpload")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(isVideoType ? "maxVideoSize" : "maxFileSize")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isUploading ? t("uploading") : t(isVideoType ? "clickToUploadVideo" : "clickToUpload")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t(isVideoType ? "maxVideoSize" : "maxFileSize")}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
@@ -981,20 +1041,20 @@ export function PromptForm({ categories, tags, initialData, initialContributors
|
||||
)}
|
||||
{promptType === "IMAGE" && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
<span>{t("outputPreview.imageUpload")}</span>
|
||||
</div>
|
||||
<MediaField form={form} t={t} />
|
||||
<MediaField form={form} t={t} promptContent={promptContent} />
|
||||
</div>
|
||||
)}
|
||||
{promptType === "VIDEO" && (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Video className="h-4 w-4" />
|
||||
<span>{t("outputPreview.videoUpload")}</span>
|
||||
</div>
|
||||
<MediaField form={form} t={t} promptType={promptType} />
|
||||
<MediaField form={form} t={t} promptType={promptType} promptContent={promptContent} />
|
||||
</div>
|
||||
)}
|
||||
{promptType === "AUDIO" && (
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Fal.ai Media Generator Plugin
|
||||
*
|
||||
* Generates images and videos using Fal.ai API.
|
||||
*
|
||||
* NOTE: This plugin is currently DISABLED and serves as a placeholder.
|
||||
*
|
||||
* Required env vars:
|
||||
* - FAL_API_KEY
|
||||
* - FAL_VIDEO_MODELS (comma-separated)
|
||||
* - FAL_IMAGE_MODELS (comma-separated)
|
||||
*/
|
||||
|
||||
import type {
|
||||
MediaGeneratorPlugin,
|
||||
MediaGeneratorModel,
|
||||
GenerationRequest,
|
||||
GenerationTask,
|
||||
WebSocketHandler,
|
||||
WebSocketCallbacks,
|
||||
GenerationStatusKey,
|
||||
} from "./types";
|
||||
|
||||
function parseModels(envVar: string | undefined, type: "image" | "video"): MediaGeneratorModel[] {
|
||||
if (!envVar) return [];
|
||||
return envVar
|
||||
.split(",")
|
||||
.map((m) => m.trim())
|
||||
.filter(Boolean)
|
||||
.map((id) => ({
|
||||
id,
|
||||
name: id.split("/").pop() || id,
|
||||
type,
|
||||
}));
|
||||
}
|
||||
|
||||
// Map Fal.ai message types to static translation keys (placeholder - uses same keys as Wiro)
|
||||
const falStatusMap: Record<string, GenerationStatusKey> = {
|
||||
pending: "queued",
|
||||
in_progress: "generating",
|
||||
completed: "complete",
|
||||
failed: "error",
|
||||
};
|
||||
|
||||
// Placeholder WebSocket handler for Fal.ai - to be implemented when enabling
|
||||
const falWebSocketHandler: WebSocketHandler = {
|
||||
getInitMessage: (_socketAccessToken: string) => {
|
||||
// Fal.ai may use a different initialization mechanism
|
||||
return "";
|
||||
},
|
||||
|
||||
handleMessage: (_event: MessageEvent, callbacks: WebSocketCallbacks) => {
|
||||
// Placeholder - Fal.ai WebSocket handling not implemented yet
|
||||
callbacks.setError("Fal.ai WebSocket handling is not implemented yet");
|
||||
},
|
||||
};
|
||||
|
||||
// Export for potential future use
|
||||
export { falStatusMap };
|
||||
|
||||
export const falGeneratorPlugin: MediaGeneratorPlugin = {
|
||||
id: "fal",
|
||||
name: "Fal.ai",
|
||||
|
||||
isConfigured: () => {
|
||||
return !!(
|
||||
process.env.FAL_API_KEY &&
|
||||
(process.env.FAL_VIDEO_MODELS || process.env.FAL_IMAGE_MODELS)
|
||||
);
|
||||
},
|
||||
|
||||
isEnabled: () => {
|
||||
// Fal.ai is disabled for now - return false even if configured
|
||||
return false;
|
||||
},
|
||||
|
||||
getModels: () => {
|
||||
// Return empty array since disabled
|
||||
if (!falGeneratorPlugin.isEnabled()) {
|
||||
return [];
|
||||
}
|
||||
const imageModels = parseModels(process.env.FAL_IMAGE_MODELS, "image");
|
||||
const videoModels = parseModels(process.env.FAL_VIDEO_MODELS, "video");
|
||||
return [...imageModels, ...videoModels];
|
||||
},
|
||||
|
||||
async startGeneration(_request: GenerationRequest): Promise<GenerationTask> {
|
||||
throw new Error(
|
||||
"Fal.ai integration is not yet implemented. Please use Wiro.ai or upload media directly."
|
||||
);
|
||||
},
|
||||
|
||||
getWebSocketUrl: () => {
|
||||
// Placeholder - Fal.ai may use a different WebSocket mechanism
|
||||
return "";
|
||||
},
|
||||
|
||||
webSocketHandler: falWebSocketHandler,
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Media Generators Plugin Registry
|
||||
*
|
||||
* Manages AI-powered media generation plugins for images and videos.
|
||||
*/
|
||||
|
||||
import { wiroGeneratorPlugin } from "./wiro";
|
||||
import { falGeneratorPlugin } from "./fal";
|
||||
import type { MediaGeneratorPlugin, MediaGeneratorModel, MediaType, WebSocketHandler } from "./types";
|
||||
|
||||
export * from "./types";
|
||||
|
||||
// Export individual plugins for direct access to handlers
|
||||
export { wiroGeneratorPlugin } from "./wiro";
|
||||
export { falGeneratorPlugin } from "./fal";
|
||||
|
||||
const mediaGeneratorRegistry = new Map<string, MediaGeneratorPlugin>();
|
||||
|
||||
let initialized = false;
|
||||
|
||||
export function registerMediaGeneratorPlugin(plugin: MediaGeneratorPlugin): void {
|
||||
mediaGeneratorRegistry.set(plugin.id, plugin);
|
||||
}
|
||||
|
||||
export function getMediaGeneratorPlugin(id: string): MediaGeneratorPlugin | undefined {
|
||||
return mediaGeneratorRegistry.get(id);
|
||||
}
|
||||
|
||||
export function getAllMediaGeneratorPlugins(): MediaGeneratorPlugin[] {
|
||||
return Array.from(mediaGeneratorRegistry.values());
|
||||
}
|
||||
|
||||
export function getEnabledMediaGeneratorPlugins(): MediaGeneratorPlugin[] {
|
||||
return getAllMediaGeneratorPlugins().filter((p) => p.isEnabled());
|
||||
}
|
||||
|
||||
export function initializeMediaGenerators(): void {
|
||||
if (initialized) return;
|
||||
|
||||
registerMediaGeneratorPlugin(wiroGeneratorPlugin);
|
||||
registerMediaGeneratorPlugin(falGeneratorPlugin);
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available models from enabled generators
|
||||
*/
|
||||
export function getAvailableModels(type?: MediaType): Array<MediaGeneratorModel & { provider: string }> {
|
||||
initializeMediaGenerators();
|
||||
|
||||
const models: Array<MediaGeneratorModel & { provider: string }> = [];
|
||||
|
||||
for (const plugin of getEnabledMediaGeneratorPlugins()) {
|
||||
const pluginModels = plugin.getModels();
|
||||
for (const model of pluginModels) {
|
||||
if (!type || model.type === type) {
|
||||
models.push({
|
||||
...model,
|
||||
provider: plugin.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any media generator is available
|
||||
*/
|
||||
export function isMediaGenerationAvailable(): boolean {
|
||||
initializeMediaGenerators();
|
||||
return getEnabledMediaGeneratorPlugins().length > 0;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Media Generator Plugin Types
|
||||
*
|
||||
* Interface definitions for AI-powered media generation plugins.
|
||||
*/
|
||||
|
||||
export type MediaType = "image" | "video";
|
||||
|
||||
export interface MediaGeneratorModel {
|
||||
id: string;
|
||||
name: string;
|
||||
type: MediaType;
|
||||
}
|
||||
|
||||
export type AspectRatio = "1:1" | "16:9" | "9:16" | "4:3" | "3:4" | "3:2" | "2:3";
|
||||
|
||||
export interface GenerationRequest {
|
||||
prompt: string;
|
||||
model: string;
|
||||
type: MediaType;
|
||||
inputImageUrl?: string;
|
||||
resolution?: string;
|
||||
aspectRatio?: AspectRatio;
|
||||
}
|
||||
|
||||
export interface GenerationTask {
|
||||
taskId: string;
|
||||
socketAccessToken: string;
|
||||
}
|
||||
|
||||
export interface GenerationProgress {
|
||||
type: string;
|
||||
message?: string;
|
||||
outputs?: Array<{ url: string }>;
|
||||
}
|
||||
|
||||
export interface GenerationResult {
|
||||
success: boolean;
|
||||
urls: string[];
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// WebSocket handler types (client-side)
|
||||
export interface WebSocketCallbacks {
|
||||
setProgress: (value: number | ((prev: number) => number)) => void;
|
||||
setStatus: (status: "idle" | "confirming" | "starting" | "queued" | "processing" | "completed" | "error") => void;
|
||||
setStatusMessage: (key: GenerationStatusKey) => void;
|
||||
setError: (error: string | null) => void;
|
||||
onComplete: (outputUrls: string[]) => void;
|
||||
onCleanup: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static generation status keys used across all providers.
|
||||
* These map to translation keys in messages/*.json under prompts.mediaGeneration.*
|
||||
*/
|
||||
export type GenerationStatusKey =
|
||||
| "connecting"
|
||||
| "connected"
|
||||
| "queued"
|
||||
| "accepted"
|
||||
| "preprocessStart"
|
||||
| "preprocessEnd"
|
||||
| "gpuAssigned"
|
||||
| "started"
|
||||
| "generating"
|
||||
| "processingOutput"
|
||||
| "ending"
|
||||
| "postprocessStart"
|
||||
| "postprocessEnd"
|
||||
| "complete"
|
||||
| "error"
|
||||
| "errorProcessing";
|
||||
|
||||
export interface WebSocketHandler {
|
||||
getInitMessage: (socketAccessToken: string) => string;
|
||||
handleMessage: (event: MessageEvent, callbacks: WebSocketCallbacks) => void;
|
||||
}
|
||||
|
||||
export interface MediaGeneratorPlugin {
|
||||
id: string;
|
||||
name: string;
|
||||
/**
|
||||
* Check if the plugin is properly configured
|
||||
*/
|
||||
isConfigured: () => boolean;
|
||||
/**
|
||||
* Check if the plugin is enabled (configured + not disabled)
|
||||
*/
|
||||
isEnabled: () => boolean;
|
||||
/**
|
||||
* Get available models for generation
|
||||
*/
|
||||
getModels: () => MediaGeneratorModel[];
|
||||
/**
|
||||
* Start a generation task and return task info for WebSocket connection
|
||||
*/
|
||||
startGeneration: (request: GenerationRequest) => Promise<GenerationTask>;
|
||||
/**
|
||||
* Get WebSocket URL for tracking progress
|
||||
*/
|
||||
getWebSocketUrl: () => string;
|
||||
/**
|
||||
* Get client-side WebSocket handler for this provider
|
||||
*/
|
||||
webSocketHandler: WebSocketHandler;
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
/**
|
||||
* Wiro.ai Media Generator Plugin
|
||||
*
|
||||
* Generates images and videos using Wiro.ai API.
|
||||
*
|
||||
* Required env vars:
|
||||
* - WIRO_API_KEY
|
||||
* - WIRO_VIDEO_MODELS (comma-separated, e.g., "google/veo3.1-fast")
|
||||
* - WIRO_IMAGE_MODELS (comma-separated, e.g., "google/nano-banana-pro,google/nano-banana")
|
||||
*/
|
||||
|
||||
import type {
|
||||
MediaGeneratorPlugin,
|
||||
MediaGeneratorModel,
|
||||
GenerationRequest,
|
||||
GenerationTask,
|
||||
WebSocketHandler,
|
||||
WebSocketCallbacks,
|
||||
GenerationStatusKey,
|
||||
} from "./types";
|
||||
|
||||
const WIRO_API_BASE = "https://api.wiro.ai/v1";
|
||||
const WIRO_SOCKET_URL = "wss://socket.wiro.ai/v1";
|
||||
|
||||
function parseModels(envVar: string | undefined, type: "image" | "video"): MediaGeneratorModel[] {
|
||||
if (!envVar) return [];
|
||||
return envVar
|
||||
.split(",")
|
||||
.map((m) => m.trim())
|
||||
.filter(Boolean)
|
||||
.map((id) => ({
|
||||
id,
|
||||
name: id.split("/").pop() || id,
|
||||
type,
|
||||
}));
|
||||
}
|
||||
|
||||
// Map Wiro message types to static translation keys
|
||||
const wiroStatusMap: Record<string, GenerationStatusKey> = {
|
||||
task_queue: "queued",
|
||||
task_accept: "accepted",
|
||||
task_preprocess_start: "preprocessStart",
|
||||
task_preprocess_end: "preprocessEnd",
|
||||
task_assign: "gpuAssigned",
|
||||
task_start: "started",
|
||||
task_output: "generating",
|
||||
task_error: "error",
|
||||
task_output_full: "processingOutput",
|
||||
task_error_full: "errorProcessing",
|
||||
task_end: "ending",
|
||||
task_postprocess_start: "postprocessStart",
|
||||
task_postprocess_end: "complete",
|
||||
};
|
||||
|
||||
const wiroWebSocketHandler: WebSocketHandler = {
|
||||
getInitMessage: (socketAccessToken: string) => {
|
||||
return JSON.stringify({
|
||||
type: "task_info",
|
||||
tasktoken: socketAccessToken,
|
||||
});
|
||||
},
|
||||
|
||||
handleMessage: (event: MessageEvent, callbacks: WebSocketCallbacks) => {
|
||||
const { setProgress, setStatus, setStatusMessage, onComplete, onCleanup } = callbacks;
|
||||
|
||||
try {
|
||||
const msg = JSON.parse(event.data);
|
||||
|
||||
if (msg.type) {
|
||||
const statusKey = wiroStatusMap[msg.type];
|
||||
if (statusKey) {
|
||||
setStatusMessage(statusKey);
|
||||
}
|
||||
|
||||
switch (msg.type) {
|
||||
case "task_queue":
|
||||
setProgress(25);
|
||||
break;
|
||||
case "task_accept":
|
||||
setProgress(30);
|
||||
break;
|
||||
case "task_preprocess_start":
|
||||
setProgress(35);
|
||||
break;
|
||||
case "task_preprocess_end":
|
||||
setProgress(40);
|
||||
break;
|
||||
case "task_assign":
|
||||
setProgress(45);
|
||||
break;
|
||||
case "task_start":
|
||||
setStatus("processing");
|
||||
setProgress(50);
|
||||
break;
|
||||
case "task_output":
|
||||
setProgress((prev) => Math.min(prev + 5, 85));
|
||||
break;
|
||||
case "task_error":
|
||||
console.error("Wiro task error:", msg.message);
|
||||
break;
|
||||
case "task_end":
|
||||
setProgress(90);
|
||||
break;
|
||||
case "task_postprocess_start":
|
||||
setProgress(92);
|
||||
break;
|
||||
case "task_postprocess_end":
|
||||
setProgress(100);
|
||||
setStatus("completed");
|
||||
|
||||
// Extract output URLs
|
||||
if (msg.message && Array.isArray(msg.message) && msg.message.length > 0) {
|
||||
const urls = msg.message.map((item: { url: string }) => item.url).filter(Boolean);
|
||||
if (urls.length > 0) {
|
||||
onComplete(urls);
|
||||
}
|
||||
}
|
||||
onCleanup();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Wiro WebSocket message parse error:", err);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const wiroGeneratorPlugin: MediaGeneratorPlugin = {
|
||||
id: "wiro",
|
||||
name: "Wiro.ai",
|
||||
|
||||
isConfigured: () => {
|
||||
return !!(
|
||||
process.env.WIRO_API_KEY &&
|
||||
(process.env.WIRO_VIDEO_MODELS || process.env.WIRO_IMAGE_MODELS)
|
||||
);
|
||||
},
|
||||
|
||||
isEnabled: () => {
|
||||
return wiroGeneratorPlugin.isConfigured();
|
||||
},
|
||||
|
||||
getModels: () => {
|
||||
const imageModels = parseModels(process.env.WIRO_IMAGE_MODELS, "image");
|
||||
const videoModels = parseModels(process.env.WIRO_VIDEO_MODELS, "video");
|
||||
return [...imageModels, ...videoModels];
|
||||
},
|
||||
|
||||
async startGeneration(request: GenerationRequest): Promise<GenerationTask> {
|
||||
if (!this.isConfigured()) {
|
||||
throw new Error(
|
||||
"Wiro.ai is not configured. Please set WIRO_API_KEY and WIRO_VIDEO_MODELS or WIRO_IMAGE_MODELS."
|
||||
);
|
||||
}
|
||||
|
||||
const apiKey = process.env.WIRO_API_KEY!;
|
||||
const url = `${WIRO_API_BASE}/Run/${request.model}`;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("prompt", request.prompt);
|
||||
|
||||
if (request.type === "video") {
|
||||
// Video-specific parameters
|
||||
formData.append("resolution", "720p");
|
||||
formData.append("generateAudio", "false");
|
||||
formData.append("enhancePrompt", "true");
|
||||
formData.append("personGeneration", "allow_adult");
|
||||
formData.append("durationSeconds", "4");
|
||||
if (request.aspectRatio) {
|
||||
formData.append("aspectRatio", request.aspectRatio);
|
||||
}
|
||||
} else {
|
||||
// Image-specific parameters
|
||||
formData.append("resolution", request.resolution || "1K");
|
||||
if (request.aspectRatio) {
|
||||
formData.append("aspectRatio", request.aspectRatio);
|
||||
}
|
||||
}
|
||||
|
||||
if (request.inputImageUrl) {
|
||||
// Fetch the image and add it to the form
|
||||
const imageResponse = await fetch(request.inputImageUrl);
|
||||
if (imageResponse.ok) {
|
||||
const imageBlob = await imageResponse.blob();
|
||||
formData.append("inputImage", imageBlob, "input.jpg");
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"x-api-key": apiKey,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Wiro.ai API error: ${response.status} - ${errorText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.result) {
|
||||
throw new Error(
|
||||
`Wiro.ai generation failed: ${result.errors?.join(", ") || "Unknown error"}`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
taskId: result.taskid,
|
||||
socketAccessToken: result.socketaccesstoken,
|
||||
};
|
||||
},
|
||||
|
||||
getWebSocketUrl: () => {
|
||||
return WIRO_SOCKET_URL;
|
||||
},
|
||||
|
||||
webSocketHandler: wiroWebSocketHandler,
|
||||
};
|
||||
Reference in New Issue
Block a user