chore(messages): update media generation prompts in multiple languages

This commit is contained in:
Fatih Kadir Akın
2025-12-25 02:43:30 +03:00
parent dcb252e31e
commit 87a34b0e8b
22 changed files with 1657 additions and 27 deletions
+10
View File
@@ -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
+37
View File
@@ -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": "إنشاء",
+37
View File
@@ -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",
+37
View File
@@ -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": "Τύπος συνημμένου μέσου",
+37
View File
@@ -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",
+37
View File
@@ -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",
+37
View File
@@ -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",
+37
View File
@@ -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": "סוג מדיה מצורף",
+37
View File
@@ -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",
+37
View File
@@ -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": "作成",
+37
View File
@@ -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": "첨부 미디어 유형",
+37
View File
@@ -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",
+37
View File
@@ -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": "Тип прикрепленного медиа",
+37
View File
@@ -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ü",
+37
View File
@@ -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": "附加媒体类型",
+89
View File
@@ -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 }
);
}
}
+451
View File
@@ -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>
</>
);
}
+86 -26
View File
@@ -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" && (
+99
View File
@@ -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,
};
+75
View File
@@ -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;
}
+107
View File
@@ -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;
}
+221
View File
@@ -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,
};