diff --git a/.env.example b/.env.example index c70c1eb9..167b1f06 100644 --- a/.env.example +++ b/.env.example @@ -41,4 +41,14 @@ NEXTAUTH_SECRET="your-super-secret-key-change-in-production" # GOOGLE_ANALYTICS_ID="G-XXXXXXXXX" # Logging (optional) -# LOG_LEVEL="info" # Options: trace, debug, info, warn, error, fatal \ No newline at end of file +# 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 \ No newline at end of file diff --git a/messages/ar.json b/messages/ar.json index 0ce38edf..ab285403 100644 --- a/messages/ar.json +++ b/messages/ar.json @@ -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": "إنشاء", diff --git a/messages/de.json b/messages/de.json index bdffa138..29b91bbe 100644 --- a/messages/de.json +++ b/messages/de.json @@ -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", diff --git a/messages/el.json b/messages/el.json index 6b4dac8d..c40cc732 100644 --- a/messages/el.json +++ b/messages/el.json @@ -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": "Τύπος συνημμένου μέσου", diff --git a/messages/en.json b/messages/en.json index 9eac7baf..d57e5c3d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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", diff --git a/messages/es.json b/messages/es.json index ddef4793..ec8b3e4b 100644 --- a/messages/es.json +++ b/messages/es.json @@ -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", diff --git a/messages/fr.json b/messages/fr.json index f92df428..21715d6e 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -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", diff --git a/messages/he.json b/messages/he.json index 5263db08..09c832fb 100644 --- a/messages/he.json +++ b/messages/he.json @@ -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": "סוג מדיה מצורף", diff --git a/messages/it.json b/messages/it.json index 4475eb64..8a301ab4 100644 --- a/messages/it.json +++ b/messages/it.json @@ -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", diff --git a/messages/ja.json b/messages/ja.json index 3d09af49..32239cc6 100644 --- a/messages/ja.json +++ b/messages/ja.json @@ -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": "作成", diff --git a/messages/ko.json b/messages/ko.json index a49b2fed..b6cb539a 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -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": "첨부 미디어 유형", diff --git a/messages/pt.json b/messages/pt.json index a6626381..15ead987 100644 --- a/messages/pt.json +++ b/messages/pt.json @@ -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", diff --git a/messages/ru.json b/messages/ru.json index f75ef761..bb501e4b 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -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": "Тип прикрепленного медиа", diff --git a/messages/tr.json b/messages/tr.json index 85288ca3..2177896e 100644 --- a/messages/tr.json +++ b/messages/tr.json @@ -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ü", diff --git a/messages/zh.json b/messages/zh.json index dbf65ba1..29fa26a3 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -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": "附加媒体类型", diff --git a/src/app/api/media-generate/route.ts b/src/app/api/media-generate/route.ts new file mode 100644 index 00000000..2e6ee3f4 --- /dev/null +++ b/src/app/api/media-generate/route.ts @@ -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 } + ); + } +} diff --git a/src/components/prompts/media-generator.tsx b/src/components/prompts/media-generator.tsx new file mode 100644 index 00000000..e31ba69e --- /dev/null +++ b/src/components/prompts/media-generator.tsx @@ -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([]); + const [isLoading, setIsLoading] = useState(true); + const [selectedModel, setSelectedModel] = useState(null); + const [aspectRatio, setAspectRatio] = useState("1:1"); + const [status, setStatus] = useState("idle"); + const [statusKey, setStatusKey] = useState(null); + const [progress, setProgress] = useState(0); + + // Get translated status message + const statusMessage = statusKey ? t(`mediaGeneration.${statusKey}`) : ""; + const [error, setError] = useState(null); + const wsRef = useRef(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 ( + + ); + } + + // 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); + + const providerNames: Record = { + wiro: "Wiro.ai", + fal: "Fal.ai", + }; + + return ( + <> +
+ + + + + + {t("chooseGenerator")} + + + {Object.entries(modelsByProvider).map(([provider, providerModels]) => ( +
+ + {providerNames[provider] || provider} + + {providerModels.map((model) => ( + { + setSelectedModel(model); + handleConfirmGenerate(); + }} + > + + {model.name} + + ))} +
+ ))} + + + + + {t("uploadInstead")} + +
+
+ + {isGenerating && ( +
+ + {statusMessage} +
+ )} +
+ + {/* Confirmation Dialog */} + !open && handleCancelGeneration()}> + + + {t("confirmGeneration")} + + {t("confirmGenerationDescription", { + provider: providerNames[selectedModel?.provider || ""] || selectedModel?.provider || "", + model: selectedModel?.name || "" + })} + + + +
+
+ {t("promptPreview")}: +

+ {prompt || t("noPromptProvided")} +

+
+ + {inputImageUrl && ( +
+ {t("inputImage")}: + Input +
+ )} + + {/* Aspect Ratio selector for image and video generation */} + {selectedModel && ( +
+ {t("aspectRatio")}: + +
+ )} +
+ + + + + +
+
+ + {/* Progress Dialog */} + {}}> + e.preventDefault()}> + + + + {t("generatingMedia")} + + + {t("doNotCloseWindow")} + + + +
+ +

+ {statusMessage} +

+
+
+
+ + {/* Result/Error Dialog */} + + + + + {status === "completed" ? ( + <> + + {t("generationComplete")} + + ) : ( + <> + + {t("generationFailed")} + + )} + + + + {status === "error" && error && ( +

{error}

+ )} + + {status === "completed" && ( +

+ {t("mediaAddedToPrompt")} +

+ )} + + + + +
+
+ + ); +} diff --git a/src/components/prompts/prompt-form.tsx b/src/components/prompts/prompt-form.tsx index 999594b5..5b0abe12 100644 --- a/src/components/prompts/prompt-form.tsx +++ b/src/components/prompts/prompt-form.tsx @@ -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>; 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("url"); const [isUploading, setIsUploading] = useState(false); const [uploadError, setUploadError] = useState(null); + const [showUpload, setShowUpload] = useState(false); const fileInputRef = useRef(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 ( ( {t("mediaUrl")} - - - +
+ {mediaUrl ? ( +
+
+ {isVideoType ? ( +
+
+ ) : ( +
+ + {showUpload && ( + + + + )} +
+ )} +
)} @@ -134,7 +184,7 @@ function MediaField({ form, t, promptType }: MediaFieldProps) { ); } - // Upload mode: show file upload + // Upload mode: show file upload with generator option return ( {t(isVideoType ? "mediaVideo" : "mediaImage")} -
+
{mediaUrl ? (
{isVideoType ? ( @@ -170,22 +220,32 @@ function MediaField({ form, t, promptType }: MediaFieldProps) {
) : ( -
fileInputRef.current?.click()} - > - {isUploading ? ( - - ) : ( - + <> + + {showUpload && ( +
fileInputRef.current?.click()} + > + {isUploading ? ( + + ) : ( + + )} +

+ {isUploading ? t("uploading") : t(isVideoType ? "clickToUploadVideo" : "clickToUpload")} +

+

+ {t(isVideoType ? "maxVideoSize" : "maxFileSize")} +

+
)} -

- {isUploading ? t("uploading") : t(isVideoType ? "clickToUploadVideo" : "clickToUpload")} -

-

- {t(isVideoType ? "maxVideoSize" : "maxFileSize")} -

-
+ )} -
+
{t("outputPreview.imageUpload")}
- +
)} {promptType === "VIDEO" && ( -
+
- +
)} {promptType === "AUDIO" && ( diff --git a/src/lib/plugins/media-generators/fal.ts b/src/lib/plugins/media-generators/fal.ts new file mode 100644 index 00000000..d0e92192 --- /dev/null +++ b/src/lib/plugins/media-generators/fal.ts @@ -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 = { + 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 { + 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, +}; diff --git a/src/lib/plugins/media-generators/index.ts b/src/lib/plugins/media-generators/index.ts new file mode 100644 index 00000000..b4994971 --- /dev/null +++ b/src/lib/plugins/media-generators/index.ts @@ -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(); + +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 { + initializeMediaGenerators(); + + const models: Array = []; + + 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; +} diff --git a/src/lib/plugins/media-generators/types.ts b/src/lib/plugins/media-generators/types.ts new file mode 100644 index 00000000..be59b96d --- /dev/null +++ b/src/lib/plugins/media-generators/types.ts @@ -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; + /** + * Get WebSocket URL for tracking progress + */ + getWebSocketUrl: () => string; + /** + * Get client-side WebSocket handler for this provider + */ + webSocketHandler: WebSocketHandler; +} diff --git a/src/lib/plugins/media-generators/wiro.ts b/src/lib/plugins/media-generators/wiro.ts new file mode 100644 index 00000000..66ce98bc --- /dev/null +++ b/src/lib/plugins/media-generators/wiro.ts @@ -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 = { + 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 { + 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, +};