File size: 5,856 Bytes
4eef090
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31bd72c
4eef090
 
 
 
 
 
 
 
 
31bd72c
4eef090
 
 
 
 
 
e480cd9
 
4eef090
 
 
 
 
 
e480cd9
 
33b76fc
e480cd9
 
4eef090
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31bd72c
4eef090
 
 
 
 
 
 
 
 
e480cd9
4eef090
 
 
 
 
 
 
31bd72c
4eef090
 
 
 
 
 
 
424b15d
 
 
 
 
 
4eef090
 
 
 
 
 
 
 
8811f65
 
 
 
 
 
 
4eef090
 
8811f65
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
166
167
168
169
170
171
172
173
174
175
176
177
import os
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from pydantic import BaseModel
from typing import List, Optional, Any

from modules.config import FEATURE_SEQUENCE, SECTIONS
from modules.core_logic import (
    generate_prompt as core_generate_prompt,
    handle_regeneration as core_handle_regeneration,
    save_character as core_save_character,
    load_character as core_load_character
)
from modules.integrations import (
    refine_master,
    generate_name_master,
    generate_image_master,
    get_ollama_models,
    check_comfy_availability
)
from modules.name_generator import generate_fantasy_name

app = FastAPI(title="Chronicle Portrait Studio API")

# Setup CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"], # Since frontend might run on different port
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

class PromptRequest(BaseModel):
    character_name: str
    features: List[str]
    randomization: List[bool]
    extra_info: List[str]

    def to_args(self):
        return [self.character_name] + self.features + self.randomization + self.extra_info

class NamingRequest(BaseModel):
    race: str

class RegenerationRequest(BaseModel):
    current_values: List[str]
    checkboxes: List[bool]
    
    def to_args(self):
        return self.current_values + self.checkboxes

class RefinementRequest(BaseModel):
    prompt: str
    backend: str
    ollama_model: Optional[str] = None
    hf_text_model: Optional[str] = None
    hf_text_provider: Optional[str] = None
    oauth_token: Optional[str] = None
    character_name: Optional[str] = None

class ImageGenerationRequest(BaseModel):
    refined_prompt: str
    technical_prompt: str
    aspect_ratio: str
    backend: str
    hf_image_model: Optional[str] = None
    hf_image_provider: Optional[str] = None
    oauth_token: Optional[str] = None
    character_name: Optional[str] = None

@app.get("/api/config")
def get_config():
    """Returns static config info needed by frontend to render dropdowns, etc."""
    from modules.core_logic import features_data, get_example_list
    from modules.integrations import gemini_active, hf_active
    from modules.config import HF_TEXT_MODELS, HF_IMAGE_MODELS
    return {
        "features_data": features_data,
        "feature_sequence": FEATURE_SEQUENCE,
        "sections": SECTIONS,
        "ollama_models": get_ollama_models(),
        "comfy_active": check_comfy_availability(),
        "gemini_active": gemini_active,
        "hf_active": hf_active,
        "is_hf_space": bool(os.environ.get("SPACE_ID")),
        "hf_text_models": HF_TEXT_MODELS,
        "hf_image_models": HF_IMAGE_MODELS,
        "examples": get_example_list()
    }

@app.get("/api/example/{filename}")
def get_example(filename: str):
    from modules.config import EXAMPLES_DIR
    import os
    path = os.path.join(EXAMPLES_DIR, filename)
    if os.path.exists(path):
        return FileResponse(path, media_type="application/json")
    raise HTTPException(status_code=404, detail="Example not found")

@app.post("/api/generate_prompt")
def generate_prompt(req: PromptRequest):
    args = req.to_args()
    prompt = core_generate_prompt(*args)
    return {"prompt": prompt}

@app.post("/api/regenerate_features")
def regenerate_features(req: RegenerationRequest):
    args = req.to_args()
    new_values = core_handle_regeneration(*args)
    return {"new_values": new_values}

@app.post("/api/generate_name")
def proxy_generate_name(req: NamingRequest):
    name = generate_fantasy_name(req.race)
    return {"name": name}

@app.post("/api/refine_prompt")
def refine_prompt(req: RefinementRequest):
    result, error_msg = refine_master(
        req.prompt, 
        req.backend, 
        req.ollama_model, 
        req.hf_text_model, 
        req.hf_text_provider, 
        req.oauth_token, 
        req.character_name
    )
    # result can be a gr.update() if it fails, and error_msg will have content
    if error_msg:
        raise HTTPException(status_code=400, detail=error_msg)
    return {"refined_prompt": result}

@app.post("/api/generate_image")
def generate_image(req: ImageGenerationRequest):
    print(f"DEBUG: Received Image Request: {req.dict()}")
    img, img_path, status_msg = generate_image_master(
        req.refined_prompt,
        req.technical_prompt,
        req.aspect_ratio,
        req.backend,
        req.hf_image_model,
        req.hf_image_provider,
        req.oauth_token,
        req.character_name
    )
    if img_path and os.path.exists(img_path):
        return FileResponse(img_path, media_type="image/png", headers={"X-Status-Msg": status_msg})
    else:
        raise HTTPException(status_code=500, detail=status_msg or "Failed to generate image.")

@app.get("/api/config/oauth")
def get_oauth_config():
    return {
        "oauth_client_id": os.environ.get("OAUTH_CLIENT_ID")
    }

@app.post("/api/save_character")
def save_character(req: PromptRequest):
    args = req.to_args()
    file_path = core_save_character(*args)
    if file_path and os.path.exists(file_path):
         return FileResponse(file_path, media_type="application/json", filename=os.path.basename(file_path))
    raise HTTPException(status_code=500, detail="Failed to save character state.")

from fastapi.staticfiles import StaticFiles
frontend_path = os.path.join(os.path.dirname(__file__), "frontend", "out")
if os.path.exists(frontend_path):
    app.mount("/", StaticFiles(directory=frontend_path, html=True), name="frontend")
else:
    print(f"WARNING: Static frontend directory not found at {frontend_path}. Please build the Next.js frontend.")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run("api:app", host="0.0.0.0", port=8000, reload=True)