Compare commits

..

1 Commits

Author SHA1 Message Date
hikari 05af2d3695 feat: add user-selectable aspect ratio and resolution per thread
Security Scan and Upload / Security & DefectDojo Upload (pull_request) Successful in 1m25s
CI / Lint & Check (pull_request) Failing after 8m33s
CI / Build Windows (pull_request) Has been skipped
Adds a two-step new thread modal: step one picks mode, step two
configures aspect ratio (Art mode only, six options) and resolution
(all modes: 1K/2K/4K). Settings are stored on the thread and forwarded
to the Gemini API on every send, retry, and edit. Also regenerates
icon.ico with Python to produce a clean all-BMP ICO compatible with
both Tauri's proc macro and llvm-rc cross-compilation.
2026-04-13 16:29:09 -07:00
8 changed files with 35 additions and 148 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "tatsumi", "name": "tatsumi",
"private": true, "private": true,
"version": "1.1.0", "version": "1.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+1 -1
View File
@@ -3716,7 +3716,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tatsumi" name = "tatsumi"
version = "1.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"dirs 5.0.1", "dirs 5.0.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "tatsumi" name = "tatsumi"
version = "1.1.0" version = "1.0.0"
description = "Tatsumi - AI art generation using Google Gemini" description = "Tatsumi - AI art generation using Google Gemini"
authors = ["Naomi Carrigan"] authors = ["Naomi Carrigan"]
edition = "2021" edition = "2021"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 MiB

After

Width:  |  Height:  |  Size: 17 MiB

+8 -21
View File
@@ -11,11 +11,11 @@ Character design (always required):
- Wavy ashen brown hair (colour and texture fixed; hairstyle can vary) - Wavy ashen brown hair (colour and texture fixed; hairstyle can vary)
- Very pale skin tone - Very pale skin tone
- Vibrant sky-blue eyes — important, commonly missed - Vibrant sky-blue eyes — important, commonly missed
- Vampire fangs (only visible when mouth is open) - Vampire fangs
- Glasses (pink-framed preferred, other styles acceptable) - Glasses (pink-framed preferred, other styles acceptable)
- Painted fingernails and toenails (any colour, never unpolished) - Painted fingernails and toenails (any colour, never unpolished)
- Slender build - Slender build
- Full body visible in frame - Full body visible in frame; always barefoot, never wears socks
Composition (always required): Composition (always required):
- Single character only - Single character only
@@ -115,31 +115,18 @@ fn build_user_gemini_parts(
} }
} }
pub struct GeminiCallParams {
pub mode: String,
pub aspect_ratio: Option<String>,
pub image_size: String,
pub user_text: Option<String>,
pub user_image_base64: Option<String>,
pub user_image_mime: Option<String>,
}
pub async fn call_gemini( pub async fn call_gemini(
api_key: String, api_key: String,
mode: String,
aspect_ratio: Option<String>,
image_size: String,
history: Vec<ThreadMessage>, history: Vec<ThreadMessage>,
params: GeminiCallParams, user_text: Option<String>,
user_image_base64: Option<String>,
user_image_mime: Option<String>,
) -> Result<(Vec<MessagePart>, f64), String> { ) -> Result<(Vec<MessagePart>, f64), String> {
let client = reqwest::Client::new(); let client = reqwest::Client::new();
let GeminiCallParams {
mode,
aspect_ratio,
image_size,
user_text,
user_image_base64,
user_image_mime,
} = params;
let is_first_message = history.is_empty(); let is_first_message = history.is_empty();
let mut contents: Vec<Value> = history let mut contents: Vec<Value> = history
+2 -5
View File
@@ -1,7 +1,7 @@
mod gemini; mod gemini;
mod storage; mod storage;
use gemini::{call_gemini, read_reference_image_base64, GeminiCallParams}; use gemini::{call_gemini, read_reference_image_base64};
use serde::Serialize; use serde::Serialize;
use storage::{ use storage::{
delete_thread_from_disk, load_config_from_disk, load_threads_from_disk, save_config_to_disk, delete_thread_from_disk, load_config_from_disk, load_threads_from_disk, save_config_to_disk,
@@ -46,7 +46,6 @@ async fn save_config(config: Config) -> Result<(), String> {
} }
#[tauri::command] #[tauri::command]
#[allow(clippy::too_many_arguments)]
async fn send_message( async fn send_message(
api_key: String, api_key: String,
mode: String, mode: String,
@@ -59,15 +58,13 @@ async fn send_message(
) -> Result<SendMessageResult, String> { ) -> Result<SendMessageResult, String> {
let (parts, cost_usd) = call_gemini( let (parts, cost_usd) = call_gemini(
api_key, api_key,
history,
GeminiCallParams {
mode, mode,
aspect_ratio, aspect_ratio,
image_size, image_size,
history,
user_text, user_text,
user_image_base64, user_image_base64,
user_image_mime, user_image_mime,
},
) )
.await?; .await?;
Ok(SendMessageResult { parts, cost_usd }) Ok(SendMessageResult { parts, cost_usd })
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "tatsumi", "productName": "tatsumi",
"version": "1.1.0", "version": "1.0.0",
"identifier": "com.naomi.tatsumi", "identifier": "com.naomi.tatsumi",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
+16 -113
View File
@@ -25,118 +25,15 @@ import type {
Thread, Thread,
} from "./types/index.ts"; } from "./types/index.ts";
const adjectives = [ const generateThreadName = (mode: Mode, text?: string): string => {
"Amber", if (
"Ancient", mode === "replace"
"Arctic", || text === undefined
"Azure", || text.trim().length === 0
"Blazing", ) {
"Celestial", return `Replace - ${new Date().toLocaleString()}`;
"Cobalt", }
"Crimson", return text.trim().slice(0, 40);
"Crystal",
"Dark",
"Distant",
"Ember",
"Emerald",
"Eternal",
"Ethereal",
"Fleeting",
"Frosted",
"Gilded",
"Golden",
"Hidden",
"Hollow",
"Indigo",
"Ivory",
"Jade",
"Lunar",
"Midnight",
"Mystic",
"Neon",
"Obsidian",
"Onyx",
"Opal",
"Pale",
"Pearl",
"Radiant",
"Rusted",
"Sapphire",
"Scarlet",
"Shattered",
"Silent",
"Silver",
"Spectral",
"Twilight",
"Velvet",
"Verdant",
"Veiled",
"Violet",
"Wandering",
"Wild",
"Woven",
"Arcane",
];
const nouns = [
"Abyss",
"Archive",
"Aria",
"Bloom",
"Cascade",
"Chronicle",
"Cipher",
"Compass",
"Crown",
"Dawn",
"Dream",
"Echo",
"Elegy",
"Ember",
"Equinox",
"Flame",
"Fragment",
"Garden",
"Horizon",
"Labyrinth",
"Lantern",
"Mirage",
"Mist",
"Murmur",
"Nexus",
"Nocturne",
"Oracle",
"Phantom",
"Prism",
"Requiem",
"Reverie",
"Ruin",
"Shadow",
"Shard",
"Silence",
"Solstice",
"Sonata",
"Specter",
"Storm",
"Tempest",
"Throne",
"Tide",
"Veil",
"Vortex",
"Whisper",
"Zenith",
"Sigil",
"Glyph",
"Dusk",
"Omen",
];
const generateThreadName = (): string => {
const adjectiveIndex = Math.floor(Math.random() * adjectives.length);
const nounIndex = Math.floor(Math.random() * nouns.length);
const adjective = adjectives[adjectiveIndex] ?? "Arcane";
const noun = nouns[nounIndex] ?? "Reverie";
return `${adjective} ${noun}`;
}; };
const generateId = (): string => { const generateId = (): string => {
@@ -177,7 +74,13 @@ const resolveUpdatedThread = (updatedThread: Thread): Thread => {
return updatedThread; return updatedThread;
} }
const name = generateThreadName(); const firstTextPart = firstMessage.parts.find((part) => {
return part.type === "text";
});
const name = generateThreadName(
updatedThread.mode,
firstTextPart?.text,
);
return { ...updatedThread, name }; return { ...updatedThread, name };
}; };