feat: add workspace content_language setting for AI generation
Users can now pick the language AI uses for captions, hashtags, descriptions, AND on-image / on-screen text — independent of the app UI locale. A user with pt-BR UI who writes for an English audience gets English content, and vice versa. - New content_language column on workspaces (added to the existing create_workspaces_table migration since we're pre-production), defaulting to 'en'. Fillable on the model. - Dropdown in Settings → Workspace → Brand, side-by-side with Tone. Options: English, Português (Brasil), Español. Short helper copy explains what the setting controls. - Validation: sometimes|string|in:en,pt-BR,es — optional on update so existing callers that don't post the field keep the current value (DB default handles fresh workspaces). - System prompt now uses $content_language (sourced from the workspace) instead of app()->getLocale(). The rule was also strengthened: the AI must always write in this language regardless of what language the user types instructions in, and must instruct image/video tools to render on-media text in this language too. - Image and video Blade prompts received a new line forcing any text rendered inside the generated media to use $content_language. - GenerateImage tool and VideoGenerationService pass the workspace's content_language into their respective prompt templates. Previously the caption would be in Portuguese (since it followed the user's input language) but the image could render with English text because there was no explicit constraint. Now the whole pipeline is aligned to one per-workspace language.
This commit is contained in:
parent
5f7768ca3a
commit
aaa4db1dbb
14 changed files with 61 additions and 25 deletions
|
|
@ -40,11 +40,16 @@ public function instructions(): string
|
|||
'brand_website' => $this->workspace->brand_website ?? '',
|
||||
'tone' => $this->workspace->brand_tone ?? 'professional',
|
||||
'voice_notes' => $this->workspace->brand_voice_notes ?? '',
|
||||
'locale' => app()->getLocale(),
|
||||
'content_language' => $this->workspace->content_language ?? 'en',
|
||||
'platform_rules' => $this->activePlatformRules(),
|
||||
])->render();
|
||||
}
|
||||
|
||||
public function contentLanguage(): string
|
||||
{
|
||||
return $this->workspace->content_language ?? 'en';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, Contract>
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ public function handle(Request $request): Stringable|string
|
|||
'brand_name' => $this->workspace->name ?? '',
|
||||
'tone' => $this->workspace->brand_tone ?? 'professional',
|
||||
'aspect_ratio' => $aspectRatio,
|
||||
'content_language' => $this->workspace->content_language ?? 'en',
|
||||
])->render();
|
||||
|
||||
$response = Image::of($renderedPrompt)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public function rules(): array
|
|||
'brand_description' => ['nullable', 'string', 'max:2000'],
|
||||
'brand_tone' => ['nullable', 'string', 'in:professional,casual,friendly,bold,inspirational,humorous,educational'],
|
||||
'brand_voice_notes' => ['nullable', 'string', 'max:2000'],
|
||||
'content_language' => ['sometimes', 'string', 'in:en,pt-BR,es'],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ class Workspace extends Model
|
|||
'brand_description',
|
||||
'brand_tone',
|
||||
'brand_voice_notes',
|
||||
'content_language',
|
||||
];
|
||||
|
||||
protected $appends = ['has_logo', 'logo_url'];
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public function generate(string $prompt, Workspace $workspace, ?string $userId =
|
|||
'prompt' => $prompt,
|
||||
'brand_name' => $workspace->name ?? '',
|
||||
'tone' => $workspace->brand_tone ?? 'professional',
|
||||
'content_language' => $workspace->content_language ?? 'en',
|
||||
])->render();
|
||||
|
||||
$operationName = $this->startGeneration($fullPrompt, $aspectRatio);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public function up(): void
|
|||
$table->text('brand_description')->nullable();
|
||||
$table->string('brand_tone')->default('professional');
|
||||
$table->text('brand_voice_notes')->nullable();
|
||||
$table->string('content_language', 10)->default('en');
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
|
||||
|
|
|
|||
|
|
@ -101,6 +101,8 @@
|
|||
'tone_educational' => 'Educational',
|
||||
'voice_notes' => 'Voice notes',
|
||||
'voice_notes_placeholder' => 'Additional writing guidelines, words to avoid, style preferences...',
|
||||
'content_language' => 'Content language',
|
||||
'content_language_description' => 'Language used for AI-generated captions, hashtags, and any text inside generated images or videos.',
|
||||
],
|
||||
|
||||
'members' => [
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -53,6 +53,7 @@ interface Workspace {
|
|||
brand_description: string | null;
|
||||
brand_tone: string;
|
||||
brand_voice_notes: string | null;
|
||||
content_language: string;
|
||||
}
|
||||
|
||||
interface Member {
|
||||
|
|
@ -77,6 +78,7 @@ const props = defineProps<{
|
|||
|
||||
const timezone = ref(props.workspace.timezone);
|
||||
const brandTone = ref(props.workspace.brand_tone ?? 'professional');
|
||||
const contentLanguage = ref(props.workspace.content_language ?? 'en');
|
||||
const inviteDialogOpen = ref(false);
|
||||
|
||||
const removeMemberModal = ref<InstanceType<typeof ConfirmDeleteModal> | null>(null);
|
||||
|
|
@ -187,26 +189,48 @@ const changeRole = (member: Member, role: string) => {
|
|||
<InputError :message="errors.brand_description" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="brand_tone">{{ $t('settings.brand.tone') }}</Label>
|
||||
<Select v-model="brandTone" name="brand_tone">
|
||||
<SelectTrigger id="brand_tone">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="professional">{{ $t('settings.brand.tone_professional') }}</SelectItem>
|
||||
<SelectItem value="casual">{{ $t('settings.brand.tone_casual') }}</SelectItem>
|
||||
<SelectItem value="friendly">{{ $t('settings.brand.tone_friendly') }}</SelectItem>
|
||||
<SelectItem value="bold">{{ $t('settings.brand.tone_bold') }}</SelectItem>
|
||||
<SelectItem value="inspirational">{{ $t('settings.brand.tone_inspirational') }}</SelectItem>
|
||||
<SelectItem value="humorous">{{ $t('settings.brand.tone_humorous') }}</SelectItem>
|
||||
<SelectItem value="educational">{{ $t('settings.brand.tone_educational') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input type="hidden" name="brand_tone" :value="brandTone" />
|
||||
<InputError :message="errors.brand_tone" />
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="brand_tone">{{ $t('settings.brand.tone') }}</Label>
|
||||
<Select v-model="brandTone" name="brand_tone">
|
||||
<SelectTrigger id="brand_tone">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="professional">{{ $t('settings.brand.tone_professional') }}</SelectItem>
|
||||
<SelectItem value="casual">{{ $t('settings.brand.tone_casual') }}</SelectItem>
|
||||
<SelectItem value="friendly">{{ $t('settings.brand.tone_friendly') }}</SelectItem>
|
||||
<SelectItem value="bold">{{ $t('settings.brand.tone_bold') }}</SelectItem>
|
||||
<SelectItem value="inspirational">{{ $t('settings.brand.tone_inspirational') }}</SelectItem>
|
||||
<SelectItem value="humorous">{{ $t('settings.brand.tone_humorous') }}</SelectItem>
|
||||
<SelectItem value="educational">{{ $t('settings.brand.tone_educational') }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input type="hidden" name="brand_tone" :value="brandTone" />
|
||||
<InputError :message="errors.brand_tone" />
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="content_language">{{ $t('settings.brand.content_language') }}</Label>
|
||||
<Select v-model="contentLanguage" name="content_language">
|
||||
<SelectTrigger id="content_language">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="pt-BR">Português (Brasil)</SelectItem>
|
||||
<SelectItem value="es">Español</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<input type="hidden" name="content_language" :value="contentLanguage" />
|
||||
<InputError :message="errors.content_language" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="-mt-4 text-xs text-muted-foreground">
|
||||
{{ $t('settings.brand.content_language_description') }}
|
||||
</p>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="brand_voice_notes">{{ $t('settings.brand.voice_notes') }}</Label>
|
||||
<Textarea
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
- Eye-catching, professional quality suitable for social media
|
||||
- Modern design with vibrant colors and clean composition
|
||||
- If including text on the image, keep it minimal, bold, and highly readable
|
||||
- ANY text that appears on the image MUST be written in {{ $content_language }}. Do not use any other language for on-image text.
|
||||
- No watermarks, no borders, no mockup frames
|
||||
- Do NOT include any offensive, violent, sexual, or inappropriate content
|
||||
@if($brand_name)
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@
|
|||
- NEVER say "I don't have access to the latest news" — create the best engaging content you can.
|
||||
- NEVER leave blanks or TODOs for the user to fill in.
|
||||
|
||||
LANGUAGE RULES:
|
||||
- The user's system language is: {{ $locale }}.
|
||||
- ALWAYS respond in the same language the user writes in.
|
||||
LANGUAGE RULES (CRITICAL):
|
||||
- The workspace's configured content language is: {{ $content_language }}.
|
||||
- ALWAYS write captions, hashtags, descriptions, and ALL your responses in this language — regardless of the language the user writes their instructions in.
|
||||
- When calling generate_image or generate_video, the prompt you pass MUST instruct the image/video generation model to render any on-image text in {{ $content_language }}. For example, prepend your image prompt with "All visible text in the image must be in {{ $content_language }}." Never let the image show text in a different language than the post itself.
|
||||
|
||||
SCOPE RULES:
|
||||
- You ONLY help with social media content creation.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
- Professional quality suitable for social media platforms
|
||||
- Modern aesthetic with good lighting and composition
|
||||
- Hook the viewer in the first 1-2 seconds
|
||||
- ANY text that appears on screen or any spoken narration MUST be in {{ $content_language }}. Do not use any other language for on-screen text or audio.
|
||||
- No watermarks, no borders
|
||||
- Do NOT include any offensive, violent, sexual, or inappropriate content
|
||||
@if($brand_name)
|
||||
|
|
|
|||
Loading…
Reference in a new issue