Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions backend/src/images/dto/create_imagen_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@ class CreateImagenDto(BaseDto):
default=False,
description="Whether to use Google Search for image generation.",
)
temperature: float | None = Field(
default=None,
ge=0.0,
le=2.0,
description=(
"Sampling temperature for Gemini image models. Lower values stay "
"closer to the prompt and the reference images, which matters when "
"editing and you want the rest of the frame preserved; higher "
"values vary more. Left unset, the model's own default applies. "
"Ignored by Imagen models, which do not expose it."
),
)
resolution: Literal["1K", "2K", "4K"] = Field(
default="1K",
description="Resolution of the generated image.",
Expand Down
6 changes: 6 additions & 0 deletions backend/src/images/imagen_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ def gemini_generate_image(
aspect_ratio: str | None = None,
google_search: bool = False,
resolution: str | None = None,
temperature: float | None = None,
) -> types.GeneratedImage | None:
"""Generates an image using the Gemini API for text-to-image or
image-to-image.
Expand Down Expand Up @@ -515,6 +516,9 @@ def gemini_generate_image(
response_modalities=["Text", "Image"],
image_config=image_config,
tools=tools if tools else None,
# Omitted when unset so the model's own default applies rather
# than us pinning one.
temperature=temperature,
)
response: types.GenerateContentResponse = (
vertexai_client.models.generate_content(
Expand Down Expand Up @@ -735,6 +739,7 @@ async def _async_worker():
aspect_ratio=request_dto.aspect_ratio,
google_search=request_dto.google_search,
resolution=request_dto.resolution,
temperature=request_dto.temperature,
)
for _ in range(request_dto.number_of_media)
]
Expand Down Expand Up @@ -811,6 +816,7 @@ async def _async_worker():
aspect_ratio=request_dto.aspect_ratio,
google_search=request_dto.google_search,
resolution=request_dto.resolution,
temperature=request_dto.temperature,
)
for _ in range(request_dto.number_of_media)
]
Expand Down
40 changes: 39 additions & 1 deletion backend/tests/images/test_imagen_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
# limitations under the License.
"""Tests for Imagen Service."""


from unittest.mock import AsyncMock, MagicMock, patch

import pytest
Expand Down Expand Up @@ -1277,3 +1276,42 @@ def test_vto_dto_validation_failures():
with pytest.raises(ValidationError) as exc_info:
VtoDto(workspace_id=1, person_image=valid_input)
assert "At least one garment" in str(exc_info.value)


def test_gemini_generate_image_forwards_temperature():
"""Temperature reaches the model when set.

Editing benefits from a low value: it keeps the result close to the
reference images so the untouched parts of the frame are preserved.
"""
mock_client = MagicMock()
mock_client.models.generate_content.return_value = MagicMock(candidates=[])

gemini_generate_image(
gcs_service=MagicMock(),
vertexai_client=mock_client,
prompt="Make the sky orange",
model=GenerationModelEnum.GEMINI_2_5_FLASH_IMAGE,
bucket_name="bucket",
temperature=0.2,
)

config = mock_client.models.generate_content.call_args.kwargs["config"]
assert config.temperature == 0.2


def test_gemini_generate_image_omits_temperature_when_unset():
"""Unset means the model's own default applies, not one we chose."""
mock_client = MagicMock()
mock_client.models.generate_content.return_value = MagicMock(candidates=[])

gemini_generate_image(
gcs_service=MagicMock(),
vertexai_client=mock_client,
prompt="A cat",
model=GenerationModelEnum.GEMINI_2_5_FLASH_IMAGE,
bucket_name="bucket",
)

config = mock_client.models.generate_content.call_args.kwargs["config"]
assert config.temperature is None
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@
<mat-icon class="!h-4 !w-4 !flex !items-center !justify-center">{{ selectedResolution() | lowercase }}</mat-icon>
</button>
}
<!--
Temperature is worth seeing at a glance while iterating on an edit,
so it sits with the other settings rather than only inside the popup.
-->
@if (supportsTemperature) {
<button
(click)="toggleSettingsMenu(); $event.stopPropagation()"
[matTooltip]="temperature === null
? 'Temperature: model default. Lower stays closer to the prompt and references.'
: 'Temperature ' + temperature + '. Lower stays closer to the prompt and references.'"
class="rounded-md bg-neutral-800 px-2 py-1 cursor-pointer hover:bg-neutral-700"
>
{{ temperature === null ? 'T auto' : 'T ' + temperature }}
</button>
}
</div>
</div>

Expand Down Expand Up @@ -479,6 +494,31 @@
}
</div>

<!-- Temperature: Gemini image models only -->
@if (supportsTemperature) {
<div class="relative">
<label for="temperature" class="block text-xs font-medium text-neutral-400 mb-1.5">
Temperature
</label>
<div class="flex items-center gap-2">
<input id="temperature" type="range" min="0" max="2" step="0.05"
[value]="temperature ?? 1"
(input)="onTemperatureInput($event)"
class="h-1 w-28 cursor-pointer accent-blue-600" />
<span class="w-8 text-right font-mono text-xs text-neutral-300">
{{ temperature === null ? 'auto' : temperature }}
</span>
<button *ngIf="temperature !== null"
(click)="temperatureChanged.emit(null)"
matTooltip="Use the model default"
class="text-[10px] text-neutral-500 hover:text-neutral-300">reset</button>
</div>
<p class="mt-1 text-[10px] text-neutral-600">
Lower stays closer to the prompt and references — useful when editing.
</p>
</div>
}

<!-- Duration Dropdown -->
@if (hasDurationOptions()) {
<div class="relative">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,16 @@ export class FlowPromptBoxComponent implements OnInit, OnDestroy {
@Output() clearReferenceVideo = new EventEmitter<Event>();
@Output() openAudioSelectorForReference = new EventEmitter<void>();
@Output() clearReferenceAudio = new EventEmitter<Event>();
@Output() temperatureChanged = new EventEmitter<number | null>();

@Input() image1Preview: string | null = null;
@Input() image2Preview: string | null = null;
@Input() referenceImages: ReferenceImage[] = [];
@Input() referenceImagesType: 'ASSET' | 'STYLE' = 'ASSET';
@Input() referenceVideo: any | null = null;
@Input() referenceAudio: any | null = null;
/** null means "use the model's default" rather than a chosen value. */
@Input() temperature: number | null = null;

@ViewChild('modeTrigger') modeTrigger!: ElementRef;
@ViewChild('modeMenu') modeMenu!: ElementRef;
Expand Down Expand Up @@ -237,6 +240,16 @@ export class FlowPromptBoxComponent implements OnInit, OnDestroy {
this.promptChanged.emit(target.value);
}

/** Whether the active model accepts a sampling temperature. */
get supportsTemperature(): boolean {
return !!this.getSelectedModelObject()?.capabilities?.supportsTemperature;
}

onTemperatureInput(event: Event): void {
const value = (event.target as HTMLInputElement).value;
this.temperatureChanged.emit(value === '' ? null : Number(value));
}

onEditOverlayClick(num?: NumPos, index?: number, ref?: ReferenceImage): void {
if (num) {
this.editImage.emit({num});
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/app/common/config/model-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export interface ModelCapability {
supportsVoice?: boolean;
supportsLanguage?: boolean;
supportsSeed?: boolean;
/**
* Whether sampling temperature can be set. Gemini image models accept it;
* Imagen does not expose it, and Gemini Omni rejects it outright.
*/
supportsTemperature?: boolean;
}

export interface GenerationModelConfig {
Expand Down Expand Up @@ -79,6 +84,7 @@ export const MODEL_CONFIGS: GenerationModelConfig[] = [
], // All
supportedResolutions: ['1K', '2K', '4K'],
supportedDurations: [],
supportsTemperature: true,
supportsGoogleSearch: true,
},
},
Expand Down Expand Up @@ -109,6 +115,7 @@ export const MODEL_CONFIGS: GenerationModelConfig[] = [
], // All
supportedResolutions: ['1K'],
supportedDurations: [],
supportsTemperature: true,
supportsGoogleSearch: true,
},
},
Expand All @@ -135,6 +142,7 @@ export const MODEL_CONFIGS: GenerationModelConfig[] = [
], // All
supportedResolutions: ['1K', '2K', '4K'],
supportedDurations: [],
supportsTemperature: true,
supportsGoogleSearch: true,
},
},
Expand All @@ -161,6 +169,7 @@ export const MODEL_CONFIGS: GenerationModelConfig[] = [
],
supportedResolutions: ['1K', '2K', '4K'],
supportedDurations: [],
supportsTemperature: true,
},
},

Expand Down
5 changes: 5 additions & 0 deletions frontend/src/app/common/models/search.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export type ImagenRequest = {
useBrandGuidelines: boolean;
enhancePrompt?: boolean;
googleSearch?: boolean;
/**
* Sampling temperature for Gemini image models. Undefined means the
* model's own default. Imagen and Gemini Omni do not accept it.
*/
temperature?: number;
resolution?: '1K' | '2K' | '4K';
};

Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/home/home.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@
(promptChanged)="onPromptChanged($event)" (aspectRatioChanged)="onAspectRatioChanged($event)"
(resolutionChanged)="onResolutionChanged($event)" (outputsChanged)="onOutputsChanged($event)"
(generateClicked)="onGenerateClicked()" (rewriteClicked)="onRewriteClicked()"
[temperature]="searchRequest.temperature ?? null"
(temperatureChanged)="onTemperatureChanged($event)"
(modelSelected)="onModelSelected($event)" (openImageSelectorForReference)="onOpenImageSelectorForReference()"
(onReferenceImageDrop)="onReferenceImageDrop($event)" (clearReferenceImage)="onClearReferenceImage($event)"
(editReferenceImage)="onEditPromptReferenceImage($event)"
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/app/home/home.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,11 @@ export class HomeComponent implements OnInit, AfterViewInit, OnDestroy {
});
}

onTemperatureChanged(value: number | null): void {
this.searchRequest.temperature = value ?? undefined;
this.saveState();
}

resetAllFilters() {
this.searchRequest = {
prompt: '',
Expand Down
Loading