Skip to content

Commit 27d1dda

Browse files
committed
feat: Batch Image Generation
1 parent 66646e9 commit 27d1dda

7 files changed

Lines changed: 108 additions & 56 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55
本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)
66

77

8+
## v5.6.0
9+
10+
### ✨ New Features
11+
- **Batch Image Generation**:
12+
- Added support for generating multiple images in a single batch (1-10 images per request).
13+
- Configurable "Batch Size" (生成数量) slider in Generation Quality settings.
14+
- Real-time sequential generation display with progress tracking (e.g., `(1/4)`, `(2/4)`) directly in the chat interface.
15+
816
## v5.5.0
917

1018
- **Sampling Methods**:

composeApp/src/commonMain/composeResources/values-zh/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
<string name="settings_cfg_scale_description">低 = 创意 | 高 = 精准</string>
5151
<string name="settings_sampler">采样方法</string>
5252
<string name="settings_sampler_desc">选择用于图像去噪的生成算法</string>
53+
<string name="settings_batch_count">生成数量</string>
54+
<string name="settings_batch_count_description">一次生成的图片数量</string>
5355
<string name="settings_current_configuration">当前配置</string>
5456
<string name="settings_advanced_title">高级设置</string>
5557
<string name="settings_advanced_subtitle">性能调优与实验性功能</string>

composeApp/src/commonMain/composeResources/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@
5151
<string name="settings_cfg_scale_description">Low = Creative | High = Precise</string>
5252
<string name="settings_sampler">Sampling Method</string>
5353
<string name="settings_sampler_desc">Select the algorithm used to denoise the image</string>
54+
<string name="settings_batch_count">Batch Size</string>
55+
<string name="settings_batch_count_description">Number of images to generate</string>
5456
<string name="settings_current_configuration">Current Configuration</string>
5557
<string name="settings_advanced_title">Advanced Settings</string>
5658
<string name="settings_advanced_subtitle">Experimental features and optimizations</string>

composeApp/src/commonMain/kotlin/org/onion/diffusion/ui/screen/HomeScreen.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,13 @@ private fun ChatMessagesList(chatMessages: List<ChatMessage>,snackbarHostState:
360360
}
361361
},
362362
onRegenerate = if (message.metadata?.containsKey("prompt") == true) {
363-
{ chatViewModel.reGenerateMessage(message) }
363+
{
364+
if (chatViewModel.isGenerating.value) {
365+
coroutineScope.launch {
366+
snackbarHostState.showSnackbar(getString(Res.string.error_no_interrupt_api))
367+
}
368+
}else chatViewModel.reGenerateMessage(message)
369+
}
364370
} else null,
365371
onCopyText = { textToCopy ->
366372
clipboardManager.setText(AnnotatedString(textToCopy))

composeApp/src/commonMain/kotlin/org/onion/diffusion/ui/screen/SettingScreen.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ import minediffusion.composeapp.generated.resources.settings_cfg_scale
6969
import minediffusion.composeapp.generated.resources.settings_cfg_scale_description
7070
import minediffusion.composeapp.generated.resources.settings_sampler
7171
import minediffusion.composeapp.generated.resources.settings_sampler_desc
72+
import minediffusion.composeapp.generated.resources.settings_batch_count
73+
import minediffusion.composeapp.generated.resources.settings_batch_count_description
7274
import minediffusion.composeapp.generated.resources.settings_current_configuration
7375
import minediffusion.composeapp.generated.resources.settings_flash_attn
7476
import minediffusion.composeapp.generated.resources.settings_flash_attn_desc
@@ -123,6 +125,7 @@ fun SettingScreen(
123125
// Direct access to mutableStateOf properties (singleton ViewModel)
124126
val currentWidth by chatViewModel.imageWidth
125127
val currentHeight by chatViewModel.imageHeight
128+
val currentBatchCount by chatViewModel.batchCount
126129
val currentSteps by chatViewModel.generationSteps
127130
val currentCfg by chatViewModel.cfgScale
128131
val currentSampler by chatViewModel.sampleMethod
@@ -197,6 +200,21 @@ fun SettingScreen(
197200
subtitle = stringResource(Res.string.settings_generation_quality_subtitle),
198201
icon = Icons.Default.Tune
199202
) {
203+
// Batch Count Slider
204+
SliderSetting(
205+
label = stringResource(Res.string.settings_batch_count),
206+
value = currentBatchCount.toFloat(),
207+
valueRange = 1f..10f,
208+
steps = 8,
209+
valueDisplay = currentBatchCount.toString(),
210+
description = stringResource(Res.string.settings_batch_count_description),
211+
onValueChange = { value ->
212+
chatViewModel.batchCount.value = value.toInt()
213+
}
214+
)
215+
216+
Spacer(modifier = Modifier.height(24.dp))
217+
200218
// Steps Slider
201219
SliderSetting(
202220
label = stringResource(Res.string.settings_steps),

composeApp/src/commonMain/kotlin/org/onion/diffusion/viewmodel/ChatViewModel.kt

Lines changed: 70 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ class ChatViewModel : ViewModel() {
7272
/** Image height - options: 128, 256, 512, 768, 1024 */
7373
var imageHeight = mutableStateOf(512)
7474

75+
/** Batch count - number of images to generate */
76+
var batchCount = mutableStateOf(1)
77+
7578
/** Steps for generation - range: 1-50 */
7679
var generationSteps = mutableStateOf(5)
7780

@@ -236,67 +239,80 @@ class ChatViewModel : ViewModel() {
236239
println("Image prompt: $promptContent")
237240
println("Image negative: $negativeContent")
238241
// Call txt2Img to generate image from the query prompt
239-
val startTime = Clock.System.now().toEpochMilliseconds()
240242

241243
val enabledLoras = loraList.filter { it.isEnabled }
242244
val loraPaths = enabledLoras.map { it.path }.toTypedArray()
243245
val loraStrengths = enabledLoras.map { it.strength }.toFloatArray()
244246

245-
val imageByteArray = diffusionLoader.txt2Img(
246-
prompt = promptContent,
247-
negative = negativeContent,
248-
// 768×1024(竖版人像)/ 1024×768(横版场景)/ 1024×1344(高清竖版)
249-
width = imageWidth.value,
250-
height = imageHeight.value,
251-
steps = generationSteps.value,//模型渲染细节的 “迭代次数”,步数越多细节越丰富,但耗时越长(20-30 步性价比最高)
252-
cfg = cfgScale.value,// 控制模型 “遵守正向提示词” 的严格程度,数值越高越贴合提示词,越低越自由发挥(7.0-9.0 最常用)
253-
seed = Clock.System.now().toEpochMilliseconds(),
254-
sampleMethod = sampleMethod.value,
255-
loraPaths = loraPaths,
256-
loraStrengths = loraStrengths
257-
)
258-
259-
// Debug logging to verify image format
260-
println("=== Image Generation Debug ===")
261-
println("Image size: ${imageByteArray?.size} bytes")
262-
if (imageByteArray != null && imageByteArray.size >= 10) {
263-
println("First 10 bytes: ${imageByteArray.take(10).joinToString { it.toString() }}")
264-
// PNG signature: 137 80 78 71 13 10 26 10 (需要使用 and 0xFF 转换为无符号值)
265-
// JPEG signature: 255 216 255
266-
val isPNG = imageByteArray.size >= 8 &&
267-
imageByteArray[0].toInt() and 0xFF == 137 &&
268-
imageByteArray[1].toInt() and 0xFF == 80 &&
269-
imageByteArray[2].toInt() and 0xFF == 78 &&
270-
imageByteArray[3].toInt() and 0xFF == 71
271-
val isJPEG = imageByteArray.size >= 3 &&
272-
imageByteArray[0].toInt() and 0xFF == 255 &&
273-
imageByteArray[1].toInt() and 0xFF == 216
274-
println("Format detection - PNG: $isPNG, JPEG: $isJPEG")
275-
}
276-
println("==============================")
277-
// Update the last message in the chat with the generated image
278-
// Using removeAt + add instead of index assignment to trigger recomposition
279-
if (_currentChatMessages.isNotEmpty()) {
280-
val lastIndex = _currentChatMessages.lastIndex
281-
_currentChatMessages.removeAt(lastIndex)
282-
val generationDuration = Clock.System.now().toEpochMilliseconds() - startTime
283-
val msg = getString(Res.string.image_generation_finished).replace("%s", formatDuration(generationDuration))
284-
val metadata = mapOf(
285-
"prompt" to promptContent,
286-
"negative_prompt" to negativeContent,
287-
"steps" to generationSteps.value.toString(),
288-
"cfg_scale" to cfgScale.value.toString(),
289-
"seed" to Clock.System.now().toEpochMilliseconds().toString(), // Note: verify if we should use the same seed as generation
290-
"model" to diffusionModelPath.value.substringAfterLast("/"),
291-
"loras" to enabledLoras.joinToString(",") { "${it.name}:${it.strength}" }
247+
var first = true
248+
for (i in 0 until batchCount.value) {
249+
val startTime = Clock.System.now().toEpochMilliseconds()
250+
val imageByteArray = diffusionLoader.txt2Img(
251+
prompt = promptContent,
252+
negative = negativeContent,
253+
// 768×1024(竖版人像)/ 1024×768(横版场景)/ 1024×1344(高清竖版)
254+
width = imageWidth.value,
255+
height = imageHeight.value,
256+
steps = generationSteps.value,//模型渲染细节的 “迭代次数”,步数越多细节越丰富,但耗时越长(20-30 步性价比最高)
257+
cfg = cfgScale.value,// 控制模型 “遵守正向提示词” 的严格程度,数值越高越贴合提示词,越低越自由发挥(7.0-9.0 最常用)
258+
seed = Clock.System.now().toEpochMilliseconds(),
259+
sampleMethod = sampleMethod.value,
260+
loraPaths = loraPaths,
261+
loraStrengths = loraStrengths
292262
)
293263

294-
_currentChatMessages.add(lastIndex, ChatMessage(
295-
message = msg,
296-
isUser = false,
297-
image = imageByteArray,
298-
metadata = metadata
299-
))
264+
// Debug logging to verify image format
265+
println("=== Image Generation Debug ===")
266+
println("Image size: ${imageByteArray?.size} bytes")
267+
if (imageByteArray != null && imageByteArray.size >= 10) {
268+
println("First 10 bytes: ${imageByteArray.take(10).joinToString { it.toString() }}")
269+
// PNG signature: 137 80 78 71 13 10 26 10 (需要使用 and 0xFF 转换为无符号值)
270+
// JPEG signature: 255 216 255
271+
val isPNG = imageByteArray.size >= 8 &&
272+
imageByteArray[0].toInt() and 0xFF == 137 &&
273+
imageByteArray[1].toInt() and 0xFF == 80 &&
274+
imageByteArray[2].toInt() and 0xFF == 78 &&
275+
imageByteArray[3].toInt() and 0xFF == 71
276+
val isJPEG = imageByteArray.size >= 3 &&
277+
imageByteArray[0].toInt() and 0xFF == 255 &&
278+
imageByteArray[1].toInt() and 0xFF == 216
279+
println("Format detection - PNG: $isPNG, JPEG: $isJPEG")
280+
}
281+
println("==============================")
282+
// Update the last message in the chat with the generated image
283+
// Using removeAt + add instead of index assignment to trigger recomposition
284+
if (_currentChatMessages.isNotEmpty()) {
285+
val lastIndex = _currentChatMessages.lastIndex
286+
if (first) {
287+
_currentChatMessages.removeAt(lastIndex)
288+
first = false
289+
} else {
290+
_currentChatMessages.removeAt(lastIndex)
291+
}
292+
val generationDuration = Clock.System.now().toEpochMilliseconds() - startTime
293+
val batchStr = if (batchCount.value > 1) " (${i + 1}/${batchCount.value})" else ""
294+
val msg = getString(Res.string.image_generation_finished).replace("%s", formatDuration(generationDuration)) + batchStr
295+
val metadata = mapOf(
296+
"prompt" to promptContent,
297+
"negative_prompt" to negativeContent,
298+
"steps" to generationSteps.value.toString(),
299+
"cfg_scale" to cfgScale.value.toString(),
300+
"seed" to Clock.System.now().toEpochMilliseconds().toString(), // Note: verify if we should use the same seed as generation
301+
"model" to diffusionModelPath.value.substringAfterLast("/"),
302+
"loras" to enabledLoras.joinToString(",") { "${it.name}:${it.strength}" }
303+
)
304+
305+
_currentChatMessages.add(ChatMessage(
306+
message = msg,
307+
isUser = false,
308+
image = imageByteArray,
309+
metadata = metadata
310+
))
311+
312+
if (i < batchCount.value - 1) {
313+
_currentChatMessages.add(ChatMessage("", false))
314+
}
315+
}
300316
}
301317
}
302318
isGenerating.value = false

gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[versions]
22
agp = "8.9.1"
3-
app-version="5.5.0"
3+
app-version="5.6.0"
44
android-compileSdk = "36"
55
android-minSdk ="29"
66
android-targetSdk = "35"

0 commit comments

Comments
 (0)