AI-powered App Store screenshot generation and localization. The asc app-shots command uses Gemini AI to produce polished marketing PNG images from your raw app screenshots and a ScreenPlan JSON file, and can translate them into any locale in one step.
Two generation workflows — pick one:
Workflow A — AI-powered (Gemini):
asc-app-shotsskill — Claude fetches App Store metadata, analyzes screenshots with vision, and writes.asc/app-shots/app-shots-plan.jsonasc app-shots generate— reads the plan + screenshots, calls Gemini image generation API in parallel, writesscreen-{index}.pngto.asc/app-shots/output/asc app-shots translate(optional) — reads the English plan + generated screenshots, recreates them with translated text for each--tolocale
Workflow B — HTML (no AI needed):
asc-app-shotsskill — same planning step as aboveasc app-shots html— generates a self-contained HTML page; open in browser to preview and export PNGs at exact App Store dimensions
Generate marketing PNG images using Gemini AI. Reads a ScreenPlan JSON file, discovers or accepts screenshot files, and outputs one PNG per screen.
| Flag | Default | Description |
|---|---|---|
--plan |
.asc/app-shots/app-shots-plan.json |
Path to the ScreenPlan JSON file |
--gemini-api-key |
— | Gemini API key (falls back to GEMINI_API_KEY env var, then stored config) |
--model |
gemini-3.1-flash-image-preview |
Gemini image generation model |
--output-dir |
.asc/app-shots/output |
Directory to write generated PNG files |
--output-width |
1320 |
Output PNG width in pixels — see Device Sizes |
--output-height |
2868 |
Output PNG height in pixels — see Device Sizes |
--device-type |
— | Named device type (e.g. APP_IPHONE_69) — overrides --output-width/--output-height |
--style-reference |
— | Path to a reference image whose visual style (colors, typography, layout) Gemini should replicate. Content is not copied — only the aesthetic. |
<screenshots> |
(auto-discovered) | Screenshot files; omit to auto-discover *.png/*.jpg from plan directory |
--output |
json |
Output format: json, table, markdown |
--pretty |
— | Pretty-print JSON output |
# Zero-argument happy path — iPhone 6.9" (APP_IPHONE_69) at 1320×2868 by default
asc app-shots generate
# Match the visual style of a reference screenshot
asc app-shots generate --style-reference ~/Downloads/competitor-shot.png
# Named device type — no need to remember pixel dimensions
asc app-shots generate --device-type APP_IPHONE_69 # 1320×2868
asc app-shots generate --device-type APP_IPHONE_67 # 1290×2796
asc app-shots generate --device-type APP_IPHONE_65 # 1260×2736
asc app-shots generate --device-type APP_IPHONE_55 # 1242×2208
asc app-shots generate --device-type APP_IPAD_PRO_129 # 2048×2732
# Style reference + device type combined
asc app-shots generate \
--style-reference ~/Downloads/competitor-shot.png \
--device-type APP_IPHONE_69
# Explicit dimensions (alternative to --device-type)
asc app-shots generate --output-width 1320 --output-height 2868
# Explicit plan + screenshots
asc app-shots generate \
--plan .asc/app-shots/app-shots-plan.json \
--output-dir .asc/app-shots/output \
.asc/app-shots/screen1.png .asc/app-shots/screen2.png
# Different model
asc app-shots generate --model gemini-2.0-flash-expJSON output:
{
"generated": [
{"screenIndex": 0, "file": ".asc/app-shots/output/screen-0.png"},
{"screenIndex": 1, "file": ".asc/app-shots/output/screen-1.png"}
]
}Table output:
| Screen | File |
|---|---|
| 0 | .asc/app-shots/output/screen-0.png |
| 1 | .asc/app-shots/output/screen-1.png |
Translate already-generated screenshots into one or more locales. The command modifies each screen's imagePrompt to include translation instructions, then calls the same Gemini generation pipeline. The existing screen-{n}.png files are sent as visual reference so Gemini keeps layout, colors, and device mockup identical — only text changes.
| Flag | Default | Description |
|---|---|---|
--plan |
.asc/app-shots/app-shots-plan.json |
Source ScreenPlan JSON |
--from |
en |
Source locale label (informational) |
--to |
(required, repeatable) | Target locale(s): --to zh --to ja --to ko |
--source-dir |
.asc/app-shots/output |
Directory containing existing screen-*.png files |
--output-dir |
.asc/app-shots/output |
Base output directory; locale subdirs are created automatically |
--output-width |
1320 |
Output PNG width in pixels — see Device Sizes |
--output-height |
2868 |
Output PNG height in pixels — see Device Sizes |
--device-type |
— | Named device type (e.g. APP_IPHONE_69) — overrides --output-width/--output-height |
--style-reference |
— | Path to a reference image whose visual style Gemini should replicate (same aesthetic as in generate) |
--gemini-api-key |
— | Gemini API key (same 3-level resolution as generate) |
--model |
gemini-3.1-flash-image-preview |
Gemini image generation model |
--output |
json |
Output format: json, table, markdown |
# Translate to Chinese and Japanese in one command
asc app-shots translate --to zh --to ja
# Keep the same visual style as the original reference during translation
asc app-shots translate --to zh --to ja --style-reference ~/Downloads/competitor-shot.png
# Explicit paths
asc app-shots translate \
--plan .asc/app-shots/app-shots-plan.json \
--source-dir .asc/app-shots/output \
--output-dir .asc/app-shots/output \
--to zh --to ja --to koJSON output:
{"data": [
{"locale": "ja", "screens": 2, "outputDir": ".asc/app-shots/output/ja", "affordances": {}},
{"locale": "zh", "screens": 2, "outputDir": ".asc/app-shots/output/zh", "affordances": {}}
]}Table output:
| Locale | Screens | Output Dir |
|---|---|---|
| ja | 2 | .asc/app-shots/output/ja |
| zh | 2 | .asc/app-shots/output/zh |
Generate a self-contained HTML page from a ScreenPlan JSON — no AI API key needed. Open the HTML in any browser to preview all screenshots and export them as PNGs at exact App Store dimensions.
| Flag | Default | Description |
|---|---|---|
--plan |
.asc/app-shots/app-shots-plan.json |
Path to the ScreenPlan JSON file |
--output-dir |
.asc/app-shots/output |
Directory to write the HTML file |
--output-width |
1320 |
Output PNG width in pixels |
--output-height |
2868 |
Output PNG height in pixels |
--device-type |
— | Named device type — overrides --output-width/--output-height |
<screenshots> |
(auto-discovered) | Screenshot files; omit to auto-discover *.png/*.jpg from plan directory |
--output |
json |
Output format: json, table, markdown |
# Zero-argument happy path
asc app-shots html
# Explicit plan + device type
asc app-shots html --plan .asc/app-shots/app-shots-plan.json --device-type APP_IPHONE_67
# Explicit screenshots
asc app-shots html --plan plan.json screen1.png screen2.pngFeatures:
- Self-contained single HTML file with base64-embedded screenshots
- CSS-only phone mockup frames — no external images needed
- Layout modes:
center,left,tilted— applied from plan - Plan colors (primary, accent, text, subtext) drive all styling
- Per-screen "Export" buttons and "Export All PNGs" toolbar button
- Uses html-to-image CDN for pixel-perfect PNG export
- Double-render pattern ensures fonts load correctly before export
JSON output:
{"file":".asc/app-shots/output/app-shots.html"}Manage the Gemini API key. Saves to ~/.asc/app-shots-config.json so you never need to pass --gemini-api-key again.
| Flag | Description |
|---|---|
--gemini-api-key KEY |
Save key to ~/.asc/app-shots-config.json |
--remove |
Delete stored config |
| (none) | Show current key (masked) and source (file or environment) |
# Save key once
asc app-shots config --gemini-api-key AIzaSy...
# Check current config
asc app-shots config
# → Gemini API key: AIzaSyBU...IpV4 (source: file)
# Remove stored key
asc app-shots config --removeKey resolution order in generate:
--gemini-api-keyflag$GEMINI_API_KEYenvironment variable~/.asc/app-shots-config.json(set viaasc app-shots config)- Error with instructions
--output-width and --output-height control the final PNG dimensions. Gemini returns images at ~704×1520; the CLI upscales to the target size using CoreGraphics before writing.
Required sizes must be present for App Store submission. Optional sizes cover older or additional devices.
| Display Type | Device | --output-width |
--output-height |
App Store |
|---|---|---|---|---|
APP_IPHONE_69 |
iPhone 6.9" | 1320 |
2868 |
✅ Required |
APP_IPHONE_67 |
iPhone 6.7" | 1290 |
2796 |
✅ Required |
APP_IPHONE_65 |
iPhone 6.5" | 1260 |
2736 |
✅ Required |
APP_IPHONE_61 |
iPhone 6.1" | 1179 |
2556 |
Optional |
APP_IPHONE_58 |
iPhone 5.8" | 1125 |
2436 |
Optional |
APP_IPHONE_55 |
iPhone 5.5" | 1242 |
2208 |
Optional |
APP_IPHONE_47 |
iPhone 4.7" | 750 |
1334 |
Optional |
APP_IPHONE_40 |
iPhone 4.0" | 640 |
1136 |
Optional |
APP_IPHONE_35 |
iPhone 3.5" | 640 |
960 |
Optional |
| Display Type | Device | --output-width |
--output-height |
App Store |
|---|---|---|---|---|
APP_IPAD_PRO_129 |
iPad 13" (12.9") | 2048 |
2732 |
✅ Required |
APP_IPAD_PRO_3GEN_11 |
iPad 11" | 1668 |
2388 |
Optional |
APP_IPAD_105 |
iPad 10.5" | 1668 |
2224 |
Optional |
APP_IPAD_97 |
iPad 9.7" | 1536 |
2048 |
Optional |
| Display Type | Device | --output-width |
--output-height |
|---|---|---|---|
APP_APPLE_TV |
Apple TV | 1920 |
1080 |
APP_DESKTOP |
Mac | 2560 |
1600 |
APP_APPLE_VISION_PRO |
Apple Vision Pro | 3840 |
2160 |
Tip: Generate each size in sequence — same plan, different
--output-dir:asc app-shots generate --device-type APP_IPHONE_69 --output-dir .asc/app-shots/output/iphone-69 asc app-shots generate --device-type APP_IPHONE_67 --output-dir .asc/app-shots/output/iphone-67 asc app-shots generate --device-type APP_IPAD_PRO_129 --output-dir .asc/app-shots/output/ipad-13
# 1. One-time: save Gemini API key
asc app-shots config --gemini-api-key AIzaSy...
# 2. Put your screenshots in the project's .asc/app-shots/ directory
cp ~/Screenshots/screen1.png .asc/app-shots/
cp ~/Screenshots/screen2.png .asc/app-shots/
# 3. Use the asc-app-shots skill in Claude Code to generate the plan:
# "Plan my App Store screenshots for app 6736834466"
# → Claude fetches metadata, analyzes screenshots, writes:
# .asc/app-shots/app-shots-plan.json
# 4. Generate English marketing images — zero arguments needed
asc app-shots generate
# 4b. (Optional) Generate matching the style of a reference screenshot
asc app-shots generate --style-reference ~/Downloads/inspiration.png
# 5. Translate to Chinese and Japanese in one command
asc app-shots translate --to zh --to ja
# 5b. (Optional) Translate while preserving the same reference style
asc app-shots translate --to zh --to ja --style-reference ~/Downloads/inspiration.png
# 6. Find output images
ls .asc/app-shots/output/
# screen-0.png screen-1.png ja/ zh/
ls .asc/app-shots/output/zh/
# screen-0.png screen-1.pngProject directory layout:
project/
└── .asc/
└── app-shots/
├── screen1.png ← source screenshots (input)
├── screen2.png
├── app-shots-plan.json ← written by asc-app-shots skill
└── output/
├── screen-0.png ← English marketing images
├── screen-1.png
├── zh/
│ ├── screen-0.png ← Chinese translations
│ └── screen-1.png
└── ja/
├── screen-0.png ← Japanese translations
└── screen-1.png
ASCCommand Infrastructure Domain
+------------------------+ +-----------------------------+ +---------------------------+
| AppShotsCommand | | GeminiScreenshotGeneration | | ScreenPlan |
| AppShotsGenerate |---->| Repository | | ScreenConfig |
| --plan | | POST generateContent | | ScreenTone |
| --output-dir | | (native Gemini API) | | LayoutMode |
| auto-discover | +-----------------------------+ | ScreenColors |
| | | FileAppShotsConfigStorage | | AppShotsConfig |
| AppShotsTranslate |---->| ~/.asc/app-shots- | | AppShotsConfigStorage |
| --to (repeatable) | | config.json | | ScreenshotGenerationRepo |
| --source-dir | +-----------------------------+ +---------------------------+
| --output-dir |
| |
| AppShotsConfig |
| --gemini-api-key |
| --remove |
+------------------------+
- Domain: Pure value types (
ScreenPlan,ScreenConfig,AppShotsConfig) and@Mockableprotocols (ScreenshotGenerationRepository,AppShotsConfigStorage) - Infrastructure:
GeminiScreenshotGenerationRepositorycalls the native GeminigenerateContentAPI (?key=query param,responseModalities: ["TEXT","IMAGE"], parallelTaskGroup). When astyleReferenceURLis provided, the reference image is placed as the first part in the request (base64 inline-data) followed by a style-guide instruction text — before the app screenshot andimagePrompt.FileAppShotsConfigStoragereads/writes JSON to~/.asc/app-shots-config.json - ASCCommand:
AppShotsGenerateauto-discovers screenshots from the plan directory when none are provided; validates--style-referencefile existence before calling Gemini.AppShotsTranslatemodifies each screen'simagePromptwith a translation instruction, forwards the samestyleReferenceURL, and processes locales in parallel.AppShotsConfigmirrors theasc auth loginpattern
Main plan model. Implements AffordanceProviding.
| Field | Type | Description |
|---|---|---|
appId |
String |
App ID (also serves as the model's id) |
appName |
String |
App display name |
tagline |
String |
Marketing tagline |
appDescription |
String? |
2-3 sentence summary for Gemini context (optional) |
tone |
ScreenTone |
Visual tone enum |
colors |
ScreenColors |
Color palette |
screens |
[ScreenConfig] |
Ordered list of screen configurations |
Affordances:
| Key | Command |
|---|---|
generate |
asc app-shots generate |
| Field | Type | Description |
|---|---|---|
id |
String |
Computed: "\(index)" |
index |
Int |
Screen order (0-based); index 0 = hero |
screenshotFile |
String |
Source screenshot filename |
heading |
String |
Main headline (3-5 words) |
subheading |
String |
Supporting text (6-12 words) |
layoutMode |
LayoutMode |
center, left, or tilted |
visualDirection |
String |
Description of what the UI shows |
imagePrompt |
String |
Full Gemini generation prompt |
| Case | Raw Value |
|---|---|
.minimal |
"minimal" |
.playful |
"playful" |
.professional |
"professional" |
.bold |
"bold" |
.elegant |
"elegant" |
| Case | Raw Value | Usage |
|---|---|---|
.center |
"center" |
Standard screens (index 1+) |
.left |
"left" |
Text left, device right |
.tilted |
"tilted" |
Hero screen (index 0) |
| Field | Type | Example |
|---|---|---|
primary |
String |
"#0A0F1E" |
accent |
String |
"#4A90E2" |
text |
String |
"#FFFFFF" |
subtext |
String |
"#94B8D4" |
| Field | Type | Description |
|---|---|---|
geminiApiKey |
String |
Stored Gemini API key |
@Mockable
public protocol ScreenshotGenerationRepository: Sendable {
func generateImages(
plan: ScreenPlan,
screenshotURLs: [URL],
styleReferenceURL: URL?
) async throws -> [Int: Data]
}screenshotURLs— matched by filename then index order againstplan.screens[n].screenshotFilestyleReferenceURL— optional path to a reference image; when provided, the repository prepends the image + a style-guide instruction to each Gemini request so the generated output replicates the reference's colors, typography, gradients, and layout patterns without copying its content
@Mockable
public protocol AppShotsConfigStorage: Sendable {
func save(_ config: AppShotsConfig) throws
func load() throws -> AppShotsConfig?
func delete() throws
}Sources/
├── Domain/ScreenshotPlans/
│ ├── ScreenTone.swift
│ ├── LayoutMode.swift
│ ├── ScreenColors.swift
│ ├── ScreenConfig.swift
│ ├── ScreenPlan.swift
│ ├── ScreenshotGenerationRepository.swift
│ ├── AppShotsConfig.swift
│ └── AppShotsConfigStorage.swift
├── Infrastructure/ScreenshotPlans/
│ ├── GeminiScreenshotGenerationRepository.swift
│ └── FileAppShotsConfigStorage.swift
└── ASCCommand/Commands/AppShots/
├── AppShotsCommand.swift
├── AppShotsGenerate.swift
├── AppShotsTranslate.swift
└── AppShotsConfig.swift
Tests/
├── DomainTests/ScreenshotPlans/
│ ├── ScreenPlanTests.swift
│ └── AppShotsConfigTests.swift
├── InfrastructureTests/ScreenshotPlans/
│ ├── GeminiScreenshotGenerationRepositoryTests.swift
│ └── FileAppShotsConfigStorageTests.swift
└── ASCCommandTests/Commands/AppShots/
├── AppShotsGenerateTests.swift
├── AppShotsTranslateTests.swift
└── AppShotsConfigTests.swift
| File | Role |
|---|---|
ASC.swift |
Registers AppShotsCommand as subcommand |
ClientProvider.swift |
makeScreenshotGenerationRepository(apiKey:model:) |
ClientProvider.swift |
makeAppShotsConfigStorage() |
| Operation | Endpoint | Repository Method |
|---|---|---|
| Generate images | POST https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={apiKey} |
generateImages(plan:screenshotURLs:) |
| Save config | ~/.asc/app-shots-config.json (local file) |
save(_:) |
| Load config | ~/.asc/app-shots-config.json (local file) |
load() |
The GeminiScreenshotGenerationRepository:
Per screen, the GeminiScreenshotGenerationRepository:
- Prepends
App context: <appName>. <tagline>. <description>to theimagePrompt - Builds
partsin this order:- (if
styleReferenceURLis set) base64inlineDataof the reference image + style-guide instruction text - (if a matching screenshot is found) base64
inlineDataof the app screenshot textcontaining the fullimagePrompt
- (if
- POSTs to the native
generateContentendpoint withresponseModalities: ["TEXT","IMAGE"] - Calls Gemini in parallel via
TaskGroup— one task per screen - Parses
candidates[0].content.parts[].inlineData.dataas base64 PNG data - Returns
[Int: Data]mapping screen index to PNG bytes
// Style reference is forwarded as a URL to the repository
@Test func `--style-reference passes reference URL to repository`() async throws {
let refFile = FileManager.default.temporaryDirectory
.appendingPathComponent("ref-\(UUID().uuidString).png")
try fakePNG.write(to: refFile)
defer { try? FileManager.default.removeItem(at: refFile) }
var capturedStyleRef: URL?
let mockRepo = MockScreenshotGenerationRepository()
given(mockRepo).generateImages(plan: .any, screenshotURLs: .any, styleReferenceURL: .any)
.willProduce { _, _, styleRef in
capturedStyleRef = styleRef
return [0: fakePNG]
}
let cmd = try AppShotsGenerate.parse([
"--plan", planPath, "--output-dir", outputDir,
"--style-reference", refFile.path
])
_ = try await cmd.execute(repo: mockRepo)
#expect(capturedStyleRef == refFile)
}
// Infrastructure: style reference image appears before screenshot in Gemini request
@Test func `generateImages includes style reference image and instruction before screenshot`() async throws {
let stub = StubHTTPClient()
stub.response = (makeNativeGeminiImageResponse(), makeHTTPResponse())
let refFile = /* temp PNG file */ URL(fileURLWithPath: "/tmp/style.png")
try fakePNGData.write(to: refFile)
let repo = GeminiScreenshotGenerationRepository(apiKey: "key", httpClient: stub)
_ = try await repo.generateImages(
plan: makeSingleScreenPlan(), screenshotURLs: [], styleReferenceURL: refFile
)
let bodyString = String(data: stub.lastRequest!.httpBody!, encoding: .utf8)!
#expect(bodyString.contains("STYLE GUIDE"))
#expect(bodyString.contains("Do NOT copy its content"))
}Run tests:
swift test --filter 'AppShotsConfigTests' # Domain + command config tests (21)
swift test --filter 'AppShotsGenerateTests' # Command generate tests (9)
swift test --filter 'AppShotsTranslateTests' # Command translate tests (9)
swift test --filter 'GeminiScreenshot' # Infrastructure tests (10)
swift test --filter 'AppShots' # All app-shots tests (52)Add --locale to fetch from a localized plan:
// In AppShotsGenerate.swift
@Option(name: .long) var locale: String = "en-US"
// Load locale-specific plan
let planURL = URL(fileURLWithPath: ".asc/app-shots/plan-\(locale).json")Add multiple style references (pick best):
// In ScreenshotGenerationRepository.swift
func generateImages(
plan: ScreenPlan,
screenshotURLs: [URL],
styleReferenceURLs: [URL] // send several references, ask Gemini to blend
) async throws -> [Int: Data]Add video output via Gemini video generation:
// In ScreenshotGenerationRepository.swift
func generateVideo(
plan: ScreenPlan,
screenshotURLs: [URL],
styleReferenceURL: URL?
) async throws -> Data