File size: 5,406 Bytes
06764cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
afc9713
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
06764cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env python3
"""
Image Agent — generates images via HuggingFace Inference API.

Routes image-generation requests from the email inbox through
FLUX.1-schnell (fast, free tier) to the HF Inference API.
Saves generated images to the Obsidian vault under Email/Images/.
"""
from __future__ import annotations

import json, os, re, datetime, io, base64, sys
from pathlib import Path
from typing import Any

import httpx

CONFIG_PATH = Path(__file__).resolve().parents[2] / "config" / "email.json"
HF_TOKEN = os.getenv("HF_TOKEN", "") or os.getenv("HF_ACCESS_TOKEN", "")
HF_API_URL = "https://api-inference.huggingface.co/models/black-forest-labs/FLUX.1-schnell"
OBSIDIAN_VAULT = Path(os.getenv("OBSIDIAN_VAULT", "/tmp/email_vault"))
IMAGE_DIR = OBSIDIAN_VAULT / "Email" / "Images"


def load_config() -> dict:
    with open(CONFIG_PATH) as f:
        return json.load(f)


def generate_image(prompt: str, width: int = 1024, height: int = 1024,
                   num_steps: int = 4, guidance_scale: float = 0.0) -> dict[str, Any]:
    """Generate an image via HF Inference API."""
    if not HF_TOKEN:
        return {"error": "HF_TOKEN not set"}

    headers = {"Authorization": f"Bearer {HF_TOKEN}"}
    payload = {
        "inputs": prompt,
        "parameters": {
            "num_inference_steps": num_steps,
            "guidance_scale": guidance_scale,
            "width": width,
            "height": height,
            "max_sequence_length": 512,
        },
    }

    try:
        with httpx.Client(timeout=120, headers=headers) as client:
            resp = client.post(HF_API_URL, json=payload)
            if resp.status_code != 200:
                return {"error": f"HF API {resp.status_code}: {resp.text[:200]}"}

            # HF returns image bytes for image-generation tasks
            img_bytes = resp.content
            if not img_bytes:
                return {"error": "Empty response from HF"}
    except httpx.ConnectError as e:
        return {"error": f"Connection failed: {str(e)}"}
    except httpx.TimeoutException:
        return {"error": "HF API request timed out"}
    except Exception as e:
        return {"error": f"Request failed: {str(e)}"}

        # Save to Obsidian vault
        IMAGE_DIR.mkdir(parents=True, exist_ok=True)
        ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        safe_prompt = re.sub(r"[^\w\s-]", "", prompt)[:40].replace(" ", "_")
        fname = f"{ts}_{safe_prompt}.png"
        fpath = IMAGE_DIR / fname

        with open(fpath, "wb") as f:
            f.write(img_bytes)

        # Also save as base64 for markdown embedding
        b64 = base64.b64encode(img_bytes).decode()

        # Write metadata
        meta = {
            "prompt": prompt,
            "width": width,
            "height": height,
            "steps": num_steps,
            "guidance_scale": guidance_scale,
            "file": str(fpath),
            "size_bytes": len(img_bytes),
            "timestamp": datetime.datetime.now().isoformat(),
        }
        meta_path = IMAGE_DIR / f"{ts}_{safe_prompt}.json"
        with open(meta_path, "w") as f:
            json.dump(meta, f, indent=2)

        return {
            "status": "success",
            "prompt": prompt,
            "file": str(fpath),
            "size_bytes": len(img_bytes),
            "base64_preview": b64[:200] + "...",
            "metadata": meta,
        }


def generate_from_email(email_data: dict) -> dict[str, Any]:
    """Process an email requesting image generation."""
    subject = email_data.get("subject", "")
    body = email_data.get("body", "")
    sender = email_data.get("sender", "")

    # Extract image prompt from email body
    # Look for explicit prompt markers
    prompt_match = re.search(
        r"(?:image|picture|generate|create|draw|make)\s*:?\s*(.+?)(?:\n|$|(?:\.|!|\?)$)",
        body, re.IGNORECASE,
    )
    if prompt_match:
        prompt = prompt_match.group(1).strip().rstrip(".")
    else:
        # Use subject + first line of body as prompt
        prompt = f"{subject}. {body[:200]}"

    result = generate_image(prompt)

    # Store in vault with email context
    if "error" not in result:
        vault_entry = {
            "source_email": {
                "subject": subject,
                "sender": sender,
                "date": email_data.get("date", ""),
            },
            "image_result": result,
        }
        meta_path = Path(result["file"]).with_suffix(".json")
        with open(meta_path, "w") as f:
            json.dump(vault_entry, f, indent=2)

    return result


def list_generated_images(limit: int = 20) -> list[dict]:
    """List generated images from the vault."""
    images = []
    if not IMAGE_DIR.exists():
        return images

    for f in sorted(IMAGE_DIR.glob("*.png"), reverse=True)[:limit]:
        meta_path = f.with_suffix(".json")
        meta = {}
        if meta_path.exists():
            with open(meta_path) as mf:
                meta = json.load(mf)
        images.append({
            "file": f.name,
            "size_bytes": f.stat().st_size,
            "metadata": meta,
        })
    return images


if __name__ == "__main__":
    # CLI test mode
    if len(sys.argv) > 1:
        result = generate_image(sys.argv[1])
        print(json.dumps(result, indent=2, default=str))
    else:
        print(json.dumps(list_generated_images(), indent=2))