mook84816 commited on
Commit
11f4053
·
verified ·
1 Parent(s): 9e485f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +303 -73
app.py CHANGED
@@ -1,84 +1,314 @@
1
- import gradio as gr
2
  import numpy as np
3
  import math
4
- from PIL import Image
5
- import requests
6
- import io
7
- import tempfile
8
  from scipy.ndimage import map_coordinates
 
9
 
10
- # --- 設定:画像生成API ---
11
- API_URL = "https://api-inference.huggingface.co/models/runwayml/stable-diffusion-v1-5"
 
 
 
 
 
 
 
12
 
13
- # --- 選択肢データ(ここを書き換えれば種類が増えます) ---
14
- MASTER_DATA = {
15
- "動物": ["柴犬", "三毛猫", "皇帝ペンギン", "レッサーパンダ", "ウーパールーパー", "メンダコ"],
16
- "食べ物": ["ぷるぷるプリン", "クリームソーダ", "ショートケーキ", "エビフライ", "目玉焼き"],
17
- "ファンタジー": ["小さな妖精", "魔法の杖", "ミミック(宝箱)", "マンドラゴラ", "光るクリスタル"],
18
- "乗り物": ["空飛ぶクジラ", "ミニ潜水艦", "レトロなプロペラ機", "おもちゃのロケット"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  }
20
 
21
- def update_subs(main_cat):
22
- return gr.update(choices=MASTER_DATA[main_cat], value=MASTER_DATA[main_cat][0])
23
-
24
- def generate_static(main_cat, sub_cat):
25
- # 背景を白に固定し、キャラを中央に寄せる「ガチガチの呪文」
26
- prompt = f"a cute {sub_cat}, {main_cat} theme, masterpiece, high quality, simple anime style, white background, solo, center composition"
27
- try:
28
- # タイムライン長めに設定してエラーを防ぐ
29
- response = requests.post(API_URL, json={"inputs": prompt}, timeout=60)
30
- if response.status_code == 200:
31
- img = Image.open(io.BytesIO(response.content)).convert("RGBA")
32
- return img, "✅ 静止画ができました!SUZURI用にはこの画像を保存してください。"
33
- else:
34
- return None, "❌ 混み合っています。もう一度「生成」を押してください。"
35
- except:
36
- return None, "❌ 通信エラーです。再度お試しください。"
37
-
38
- def make_gif(static_img, face_protection):
39
- if static_img is None: return None, "先に画像を生成してください"
40
-
41
- # X投稿用に少しサイズを調整
42
- img_arr = np.array(static_img.resize((400, 400), Image.LANCZOS))
 
 
 
 
 
 
 
 
 
43
  frames = []
44
-
45
- # アニメーション計算
46
- for i in range(20):
47
- h, w = img_arr.shape[:2]
48
- y, x = np.indices((h, w))
49
- dist = np.sqrt((x/w - 0.5)**2 + (y/h - 0.5)**2)
50
- weight = np.clip(dist * 2, 0, 1) if face_protection else 1.0
51
- sx = 10 * math.sin(i * 0.3) * (y / h) * weight
52
- sy = 5 * math.sin(i * 0.4) * (1 - y / h) * weight
53
- map_x, map_y = (x + sx).astype(np.float32), (y + sy).astype(np.float32)
54
- warped = np.zeros_like(img_arr)
55
- for c in range(4):
56
- warped[:,:,c] = map_coordinates(img_arr[:,:,c], [map_y, map_x], order=1)
57
- frames.append(Image.fromarray(warped).convert("RGB")) # GIFはRGB
58
-
59
- tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".gif")
60
- frames[0].save(tmp.name, save_all=True, append_images=frames[1:], duration=80, loop=0)
61
- return tmp.name, "✅ X宣伝用のGIFが完成しました!"
62
-
63
- with gr.Blocks() as demo:
64
- gr.Markdown("# 🎨 SUZURI入稿 X宣伝用メーカー")
65
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  with gr.Row():
67
- with gr.Column():
68
- m_cat = gr.Dropdown(list(MASTER_DATA.keys()), value="動物", label="1. ジャンル")
69
- s_cat = gr.Dropdown(MASTER_DATA["動"], value="柴犬", label="2. キャラを選択")
70
- m_cat.change(update_subs, m_cat, s_cat)
71
-
72
- gen_btn = gr.Button("🚀 キャラを生成", variant="primary")
73
- face_p = gr.Checkbox(value=True, label="顔を歪ませない自然な動き)")
74
- gif_btn = gr.Button("🐦 X投稿用GIFを作る")
75
-
76
- with gr.Column():
77
- static_out = gr.Image(label="SUZURI用(静止画)", type="pil")
78
- gif_out = gr.Image(label="X宣伝用(GIFアニメ)")
79
- status = gr.Textbox(label="状況")
80
-
81
- gen_btn.click(generate_static, [m_cat, s_cat], [static_out, status])
82
- gif_btn.click(make_gif, [static_out, face_p], [gif_out, status])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
  demo.launch()
 
1
+ cimport gradio as gr
2
  import numpy as np
3
  import math
4
+ from PIL import Image, ImageDraw
 
 
 
5
  from scipy.ndimage import map_coordinates
6
+ import tempfile, os
7
 
8
+ def flatten_to_white(image, bg_color=(255, 255, 255)):
9
+ if image.mode == 'RGBA':
10
+ bg = Image.new('RGB', image.size, bg_color)
11
+ bg.paste(image, mask=image.split()[3])
12
+ return bg
13
+ else:
14
+ bg = Image.new('RGB', image.size, bg_color)
15
+ bg.paste(image.convert('RGB'))
16
+ return bg
17
 
18
+ def auto_suggest(image):
19
+ if image is None:
20
+ return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), "画像をアップロードしてください"
21
+
22
+ arr = np.array(image.convert('RGBA'))
23
+ h, w = arr.shape[:2]
24
+ has_alpha = image.mode == 'RGBA' and arr[:,:,3].min() < 255
25
+
26
+ if has_alpha:
27
+ mask = arr[:,:,3] > 10
28
+ else:
29
+ gray = np.array(image.convert('L'))
30
+ mask = gray < 240
31
+
32
+ if mask.any():
33
+ ys, xs = np.where(mask)
34
+ cy = ys.mean() / h
35
+ cx = xs.mean() / w
36
+ width_ratio = (xs.max() - xs.min()) / w
37
+ height_ratio = (ys.max() - ys.min()) / h
38
+ else:
39
+ cy, cx, width_ratio, height_ratio = 0.5, 0.5, 0.8, 0.8
40
+
41
+ do_breath = True
42
+ do_sway = width_ratio > 0.4
43
+ do_tail = cx > 0.55
44
+ do_fin = width_ratio > 0.6
45
+ do_bulge = height_ratio > 0.6 and width_ratio > 0.5
46
+ do_blink = True
47
+ do_bounce = height_ratio < 0.6
48
+ do_doze = cy < 0.45
49
+ do_swim = width_ratio > 0.65
50
+ do_float = True
51
+ do_nod = cy < 0.5
52
+ do_startle = False
53
+
54
+ suggestions = []
55
+ if do_breath: suggestions.append("呼吸")
56
+ if do_sway: suggestions.append("ゆらゆら")
57
+ if do_blink: suggestions.append("まばたき")
58
+ if do_bounce: suggestions.append("ぽよん")
59
+ if do_swim: suggestions.append("泳ぐ")
60
+ if do_float: suggestions.append("浮き沈み")
61
+
62
+ msg = f"提案: {', '.join(suggestions)}"
63
+ return (
64
+ gr.update(value=do_breath),
65
+ gr.update(value=do_sway),
66
+ gr.update(value=do_tail),
67
+ gr.update(value=do_fin),
68
+ gr.update(value=do_bulge),
69
+ gr.update(value=do_blink),
70
+ gr.update(value=do_bounce),
71
+ gr.update(value=do_doze),
72
+ gr.update(value=do_swim),
73
+ gr.update(value=do_float),
74
+ gr.update(value=do_nod),
75
+ gr.update(value=do_startle),
76
+ msg
77
+ )
78
+
79
+ def warp_frame(arr, W, H, tail_deg, fin_deg, breath_scale,
80
+ tail_root_x, tail_root_y, fin_root_x, body_cx, body_cy,
81
+ swim_phase, nod_deg):
82
+ ys, xs = np.mgrid[0:H, 0:W].astype(np.float64)
83
+ src_y = ys.copy(); src_x = xs.copy()
84
+
85
+ # 呼吸膨らみ
86
+ dx_b = xs - body_cx; dy_b = ys - body_cy
87
+ body_inf = np.clip(1.0 - np.sqrt(dx_b**2+dy_b**2)/( W*0.38), 0, 1)**1.2
88
+ sf = (breath_scale - 1.0) * body_inf
89
+ src_x -= dx_b * sf; src_y -= dy_b * sf
90
+
91
+ # しっぽ
92
+ if tail_deg != 0:
93
+ tail_rad = math.radians(tail_deg)
94
+ dx_t = xs - tail_root_x
95
+ inf_t = np.where(dx_t>0, (1-np.exp(-dx_t/( W*0.09)))*np.clip(dx_t/(W*0.20),0,1), 0)
96
+ src_y -= inf_t * math.sin(tail_rad) * W * 0.037
97
+
98
+ # ひれ
99
+ if fin_deg != 0:
100
+ fin_rad = math.radians(fin_deg)
101
+ dx_f = fin_root_x - xs
102
+ inf_f = np.where(dx_f>0, (1-np.exp(-dx_f/(W*0.058)))*np.clip(dx_f/(W*0.13),0,1), 0)
103
+ src_y -= inf_f * math.sin(fin_rad) * W * 0.023
104
+
105
+ # 泳ぐ(体全体が波打つ)
106
+ if swim_phase != 0:
107
+ wave = np.sin(xs / W * math.pi * 2 + swim_phase) * H * 0.015
108
+ src_y -= wave
109
+
110
+ # 頭ふり
111
+ if nod_deg != 0:
112
+ nod_rad = math.radians(nod_deg)
113
+ upper = np.clip((body_cy - ys) / (body_cy * 0.8), 0, 1)
114
+ src_x -= upper * math.sin(nod_rad) * W * 0.04
115
+
116
+ coords = [src_y.ravel(), src_x.ravel()]
117
+ result = np.full_like(arr, 255)
118
+ for c in range(arr.shape[2]):
119
+ warped = map_coordinates(arr[:,:,c].astype(np.float64), coords, order=2, mode='constant', cval=255)
120
+ result[:,:,c] = np.clip(warped.reshape(H,W), 0, 255)
121
+ return result.astype(np.uint8)
122
+
123
+ BG_COLORS = {
124
+ "白": (255, 255, 255),
125
+ "クリーム": (255, 248, 220),
126
+ "水色": (200, 230, 255),
127
+ "ピンク": (255, 220, 230),
128
+ "黒": (30, 30, 30),
129
+ }
130
+
131
+ SIZE_MAP = {
132
+ "小 (240px)": 240,
133
+ "中 (480px)": 480,
134
+ "大 (720px)": 720,
135
  }
136
 
137
+ def generate(image,
138
+ do_breath, do_sway, do_tail, do_fin, do_bulge,
139
+ do_blink, do_bounce, do_doze, do_swim, do_float, do_nod, do_startle,
140
+ intensity, speed, fmt,
141
+ breath_amp, sway_amp, tail_amp, fin_amp, bulge_amp,
142
+ bg_color_name, out_size_name):
143
+ if image is None:
144
+ return None, None, "画像アップロードしてください"
145
+
146
+ OUT = SIZE_MAP.get(out_size_name, 480)
147
+ bg_color = BG_COLORS.get(bg_color_name, (255, 255, 255))
148
+
149
+ flat = flatten_to_white(image, bg_color).resize((OUT, OUT), Image.LANCZOS)
150
+ base_arr = np.array(flat).astype(np.float64)
151
+ H, W = base_arr.shape[:2]
152
+
153
+ tail_root_x = int(W * 0.63)
154
+ tail_root_y = int(H * 0.50)
155
+ fin_root_x = int(W * 0.26)
156
+ body_cx = int(W * 0.50)
157
+ body_cy = int(H * 0.50)
158
+
159
+ speed_map = {"遅い": 3.0, "普通": 2.0, "速い": 1.3}
160
+ total_sec = speed_map.get(speed, 2.0)
161
+ N_FRAMES = 48
162
+ DURATION = int(total_sec * 1000 / N_FRAMES)
163
+ scale = intensity / 5.0
164
+
165
+ # びくっのタイミング
166
+ startle_frame = N_FRAMES // 4 if do_startle else -1
167
+
168
  frames = []
169
+ for i in range(N_FRAMES):
170
+ t = i / N_FRAMES
171
+ sin_t = math.sin(2*math.pi*t)
172
+ sin_t2 = math.sin(2*math.pi*t*0.5 + 0.5)
173
+
174
+ # 基本動き
175
+ breath_y = int(sin_t * breath_amp * scale) if do_breath else 0
176
+ sway_x = int(sin_t2 * sway_amp * scale) if do_sway else 0
177
+ breath_scale = (1.0 + sin_t * (bulge_amp/100) * scale) if do_bulge else 1.0
178
+ tail_a = math.sin(2*math.pi*t*1.4) * tail_amp * scale if do_tail else 0
179
+ fin_a = math.sin(2*math.pi*t*1.4 + math.pi*0.8) * fin_amp * scale if do_fin else 0
180
+
181
+ # 泳ぐ
182
+ swim_phase = (2*math.pi*t*2) if do_swim else 0
183
+
184
+ # 頭ふり
185
+ nod_deg = math.sin(2*math.pi*t*0.7) * 8 * scale if do_nod else 0
186
+
187
+ # ぽよん(バウンド)
188
+ if do_bounce:
189
+ bounce = abs(math.sin(2*math.pi*t)) * 6 * scale
190
+ breath_y += int(bounce)
191
+
192
+ # 浮き沈み(ゆっくり上下)
193
+ if do_float:
194
+ breath_y += int(math.sin(2*math.pi*t*0.5) * 5 * scale)
195
+
196
+ # うとうと(頭が下がる)
197
+ if do_doze:
198
+ doze = max(0, math.sin(2*math.pi*t*0.3)) * 10 * scale
199
+ breath_y += int(doze)
200
+
201
+ # びくっ
202
+ if do_startle and i == startle_frame:
203
+ breath_y -= int(15 * scale)
204
+ sway_x += int(5 * scale)
205
+
206
+ warped = warp_frame(
207
+ base_arr, W, H,
208
+ tail_a, fin_a, breath_scale,
209
+ tail_root_x, tail_root_y, fin_root_x,
210
+ body_cx, body_cy,
211
+ swim_phase, nod_deg
212
+ )
213
+
214
+ frame = Image.new('RGB', (OUT, OUT), bg_color)
215
+ wimg = Image.fromarray(warped)
216
+ frame.paste(wimg, (sway_x, breath_y))
217
+
218
+ # まばたき(目の部分を横線で潰す)
219
+ if do_blink:
220
+ blink_t = (math.sin(2*math.pi*t*0.4 - math.pi/2) + 1) / 2
221
+ if blink_t > 0.85:
222
+ draw = ImageDraw.Draw(frame)
223
+ eye_y = int(H * 0.38)
224
+ eye_h = int(H * 0.06 * (1 - blink_t) * 8)
225
+ if eye_h > 0:
226
+ draw.rectangle([int(W*0.25), eye_y, int(W*0.75), eye_y + eye_h],
227
+ fill=bg_color)
228
+
229
+ frames.append(frame)
230
+
231
+ suffix = ".webp" if fmt == "WebP(推奨)" else ".gif"
232
+ tmp = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
233
+
234
+ if fmt == "WebP(推奨)":
235
+ frames[0].save(tmp.name, save_all=True, append_images=frames[1:],
236
+ loop=0, duration=DURATION, lossless=True, quality=100, format='WEBP')
237
+ else:
238
+ frames[0].save(tmp.name, save_all=True, append_images=frames[1:],
239
+ loop=0, duration=DURATION, optimize=True)
240
+
241
+ size_kb = os.path.getsize(tmp.name) // 1024
242
+ msg = f"完成! {N_FRAMES}フレーム × {DURATION}ms = {total_sec:.1f}秒ループ ({size_kb}KB)"
243
+ return tmp.name, tmp.name, msg
244
+
245
+
246
+ with gr.Blocks(title="キャラアニメーター") as demo:
247
+ gr.Markdown("## キャラクターアニメーター")
248
+ gr.Markdown("画像をアップロードして、しぐさを選んでアニメーションを生成します。")
249
+
250
  with gr.Row():
251
+ with gr.Column(scale=1):
252
+ image_input = gr.Image(type="pil", image_mode="RGBA", label="画像をアップロード")
253
+ auto_btn = gr.Button("🔍 しぐさを自提案", variant="secondary")
254
+ suggest_msg = gr.Textbox(label="提案結果", interactive=False)
255
+
256
+ gr.Markdown("### 汎用しぐさ")
257
+ do_breath = gr.Checkbox(label="呼吸上下)", value=True)
258
+ do_sway = gr.Checkbox(label="ゆらゆら(左右)", value=True)
259
+ do_blink = gr.Checkbox(label="まばたき", value=False)
260
+ do_bounce = gr.Checkbox(label="ぽよん(バウンド)", value=False)
261
+ do_nod = gr.Checkbox(label="頭ふり", value=False)
262
+ do_doze = gr.Checkbox(label="うとうと", value=False)
263
+ do_startle = gr.Checkbox(label="びくっ(驚き)", value=False)
264
+
265
+ gr.Markdown("### 海の生き物系しぐさ")
266
+ do_tail = gr.Checkbox(label="しっぽふりふり", value=False)
267
+ do_fin = gr.Checkbox(label="ひれパタパタ", value=False)
268
+ do_bulge = gr.Checkbox(label="ぷくぷく膨らむ", value=False)
269
+ do_swim = gr.Checkbox(label="泳ぐ(波打つ)", value=False)
270
+ do_float = gr.Checkbox(label="浮き沈み", value=False)
271
+
272
+ gr.Markdown("### 基本調整")
273
+ intensity = gr.Slider(1, 10, value=5, step=1, label="動きの大きさ(全体)")
274
+ speed = gr.Radio(["遅い", "普通", "速い"], value="普通", label="速さ")
275
+
276
+ gr.Markdown("### 細かい動きの調整")
277
+ breath_amp = gr.Slider(1, 20, value=8, step=1, label="呼吸の幅(px)")
278
+ sway_amp = gr.Slider(1, 15, value=3, step=1, label="ゆらゆらの幅(px)")
279
+ tail_amp = gr.Slider(1, 30, value=13, step=1, label="しっぽの角度")
280
+ fin_amp = gr.Slider(1, 20, value=9, step=1, label="ひれの角度")
281
+ bulge_amp = gr.Slider(1, 10, value=4, step=1, label="膨らみの強さ(%)")
282
+
283
+ gr.Markdown("### 出力設定")
284
+ bg_color_name = gr.Radio(list(BG_COLORS.keys()), value="白", label="背景色")
285
+ out_size_name = gr.Radio(list(SIZE_MAP.keys()), value="中 (480px)", label="出力サイズ")
286
+ fmt = gr.Radio(["WebP(推奨)", "GIF"], value="WebP(推奨)", label="出力形式")
287
+
288
+ btn = gr.Button("アニメーション生成", variant="primary")
289
+
290
+ with gr.Column(scale=1):
291
+ preview = gr.Image(label="プレビュー", type="filepath")
292
+ output_file = gr.File(label="ダウンロード")
293
+ status_msg = gr.Textbox(label="ステータス", interactive=False)
294
+
295
+ auto_btn.click(
296
+ fn=auto_suggest,
297
+ inputs=[image_input],
298
+ outputs=[do_breath, do_sway, do_tail, do_fin, do_bulge,
299
+ do_blink, do_bounce, do_doze, do_swim, do_float, do_nod, do_startle,
300
+ suggest_msg]
301
+ )
302
+
303
+ btn.click(
304
+ fn=generate,
305
+ inputs=[image_input,
306
+ do_breath, do_sway, do_tail, do_fin, do_bulge,
307
+ do_blink, do_bounce, do_doze, do_swim, do_float, do_nod, do_startle,
308
+ intensity, speed, fmt,
309
+ breath_amp, sway_amp, tail_amp, fin_amp, bulge_amp,
310
+ bg_color_name, out_size_name],
311
+ outputs=[output_file, preview, status_msg]
312
+ )
313
 
314
  demo.launch()