(options: VariantsOptions)
| 138 | * Generate N variants with staggered parallel execution. |
| 139 | */ |
| 140 | export async function variants(options: VariantsOptions): Promise<void> { |
| 141 | const apiKey = requireApiKey(); |
| 142 | const baseBrief = options.briefFile |
| 143 | ? parseBrief(options.briefFile, true) |
| 144 | : parseBrief(options.brief!, false); |
| 145 | |
| 146 | const quality = options.quality || "high"; |
| 147 | |
| 148 | fs.mkdirSync(options.outputDir, { recursive: true }); |
| 149 | |
| 150 | // If viewports specified, generate responsive variants instead of style variants |
| 151 | if (options.viewports) { |
| 152 | await generateResponsiveVariants(apiKey, baseBrief, options.outputDir, options.viewports, quality); |
| 153 | return; |
| 154 | } |
| 155 | |
| 156 | const count = Math.min(options.count, 7); // Cap at 7 style variations |
| 157 | const size = options.size || "1536x1024"; |
| 158 | |
| 159 | console.error(`Generating ${count} variants...`); |
| 160 | const startTime = Date.now(); |
| 161 | |
| 162 | // Staggered parallel: start each call 1.5s apart |
| 163 | const promises: Promise<{ path: string; success: boolean; error?: string }>[] = []; |
| 164 | |
| 165 | for (let i = 0; i < count; i++) { |
| 166 | const variation = STYLE_VARIATIONS[i] || ""; |
| 167 | const prompt = variation |
| 168 | ? `${baseBrief}\n\nStyle direction: ${variation}` |
| 169 | : baseBrief; |
| 170 | |
| 171 | const outputPath = path.join(options.outputDir, `variant-${String.fromCharCode(65 + i)}.png`); |
| 172 | |
| 173 | // Stagger: wait 1.5s between launches |
| 174 | const delay = i * 1500; |
| 175 | promises.push( |
| 176 | new Promise(resolve => setTimeout(resolve, delay)) |
| 177 | .then(() => { |
| 178 | console.error(` Starting variant ${String.fromCharCode(65 + i)}...`); |
| 179 | return generateVariant(apiKey, prompt, outputPath, size, quality); |
| 180 | }) |
| 181 | ); |
| 182 | } |
| 183 | |
| 184 | const results = await Promise.allSettled(promises); |
| 185 | const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); |
| 186 | |
| 187 | const succeeded: string[] = []; |
| 188 | const failed: string[] = []; |
| 189 | |
| 190 | for (const result of results) { |
| 191 | if (result.status === "fulfilled" && result.value.success) { |
| 192 | const size = fs.statSync(result.value.path).size; |
| 193 | console.error(` ✓ ${path.basename(result.value.path)} (${(size / 1024).toFixed(0)}KB)`); |
| 194 | succeeded.push(result.value.path); |
| 195 | } else { |
| 196 | const error = result.status === "fulfilled" ? result.value.error : (result.reason as Error).message; |
| 197 | const filePath = result.status === "fulfilled" ? result.value.path : "unknown"; |
no test coverage detected