"""Character View Sheet: 11 views, optional mannequin guides, PNG-only export.""" import json import random import tempfile import time from pathlib import Path import gradio as gr from backend import MAX_GPU_SECONDS, QUOTA_MULTIPLIER, USE_ANGLES, run_gpu from core import build_zip, prepare_reference, requested_seconds from pose_guides import ensure_guides, get_guide from views import BY_KEY, DEFAULT_KEYS, SIZES, VIEWS, validate_keys GUIDES = ensure_guides() def allocation_message(keys): if not keys: return "視点を選択してください。" allocation = requested_seconds(keys, MAX_GPU_SECONDS) quota = allocation * QUOTA_MULTIPLIER message = ( f"選択: {len(set(keys))}視点。1回のGPU時間枠は最大{allocation}秒" f"(利用枠への換算: {quota}秒)。\n\n" "未ログインの1日枠は120秒です。他のZeroGPU利用分と共有されます。" "11視点の完成時間は設定や混雑で変わります。余裕がなくなったら、完成済み画像を返します。" ) if quota > 120: message += "\n\n現在のGPU設定では未ログインの1日枠を超えます。" return message def pose_preview(keys): return [(str(GUIDES[key]), BY_KEY[key].label) for key in (keys or []) if key in BY_KEY] def generate(reference, keys, pose_enabled, resolution, extra, seed, random_seed, crop_close_views=False): # Invalid retry inputs must not leave a previous character's ZIP visible. yield [], None, None, "入力を確認しています。", seed if reference is None: raise gr.Error("キャラクターの参照画像をアップロードしてください。") try: keys = validate_keys(keys) except ValueError as exc: raise gr.Error(str(exc)) from exc if resolution not in SIZES: raise gr.Error("解像度を選択してください。") seed = random.randint(0, 2**31 - 1) if random_seed else int(seed) reference = prepare_reference(reference, 768 if resolution == "fast" else 1216) guides = None if pose_enabled: guides = {key: get_guide(key, *SIZES[resolution][BY_KEY[key].shape]) for key in keys} # Clear stale downloads and results before requesting GPU time. yield [], None, None, "生成中です。", seed started = time.perf_counter() try: images, reason, metrics = run_gpu(reference, keys, guides, resolution, seed, extra or "", crop_close_views) except Exception as exc: print(f"[app] GPU request failed: {type(exc).__name__}: {exc}", flush=True) # Keep infrastructure quota errors visible; no invented successful result. yield [], None, None, f"生成できませんでした: {exc}", seed return metrics["request_wall_seconds"] = round(time.perf_counter() - started, 3) gallery = [(images[key], BY_KEY[key].label) for key in keys if key in images] status = f"{len(images)}/{len(keys)}視点を生成しました。Seed: {seed}" if reason: status += "\n" + reason # Gradio caches yielded files before requesting the next generator item. # Keep originals alive across this yield, then remove both report and ZIP. with tempfile.TemporaryDirectory(prefix="view-result-") as result_dir: report = Path(result_dir) / "generation_metrics.json" report.write_text(json.dumps(metrics, ensure_ascii=False, indent=2), encoding="utf-8") archive = build_zip(images, keys, output_dir=result_dir) yield gallery, archive, str(report), status, seed with gr.Blocks(title="Character View Sheet (Qwen Image 2.1 turbo)", delete_cache=(3600, 3600)) as demo: gr.Markdown("# Character View Sheet (Qwen Image 2.1 turbo)\n参照画像とポーズ人形から、学習用の11視点を生成します。") gr.Markdown( "**研究用デモ** · 多視点生成の速度・人物同一性・ポーズ追従を評価するための試作です。" "利用モデルのライセンスは " "[Qwen Research License](https://huggingface.co/Qwen/Qwen-Image-2.1-Turbo/blob/main/LICENSE)です。" "商用利用は別許諾が必要です。生成画像から学習したモデルを配布・提供する場合、" "関連文書に「Built with Qwen」または「Improved using Qwen」の表示条件があります。" ) with gr.Row(): with gr.Column(): reference = gr.Image(label="キャラクターの参照画像", type="pil", height=380) pose_enabled = gr.Checkbox(label="ポーズ人形を参照する", value=True) crop_close_views = gr.Checkbox(label="白背景の直立全身画像:顔・上半身用に切り出す", value=False) gr.Markdown("切り出しは実験機能です。構図が改善する一方、衣装の形が変わる場合があります。") gr.Markdown("人形の姿勢・向き・構図を参考にします。関節位置を厳密に固定する機能ではありません。") extra = gr.Textbox(label="追加の指示", placeholder="例: 元画像の線画と塗り方を維持", lines=2, max_lines=4) with gr.Column(): keys = gr.CheckboxGroup(label="生成する視点", choices=[(v.label, v.key) for v in VIEWS], value=DEFAULT_KEYS) resolution = gr.Radio( label="画像サイズ", choices=[("高速: 全身512×768・寄り640×640", "fast"), ("標準・速度優先: 全身832×1216・寄り1024×1024", "balanced"), ("標準・細部優先: 全身832×1216・寄り1024×1024", "standard")], value="balanced", ) allocation = gr.Markdown(allocation_message(DEFAULT_KEYS)) with gr.Accordion("ポーズ人形を確認", open=False): previews = gr.Gallery(value=pose_preview(DEFAULT_KEYS), columns=4, height=430, label="ポーズ参照", object_fit="contain", interactive=False) with gr.Accordion("Seed設定", open=False): seed = gr.Number(label="Seed", value=0, precision=0, minimum=0, maximum=2**31 - 1) random_seed = gr.Checkbox(label="ランダムSeed", value=True) run = gr.Button("選択した視点を生成", variant="primary") status = gr.Textbox(label="進行状況", interactive=False) results = gr.Gallery(label="生成した画像", columns=4, height=580, object_fit="contain") download = gr.File(label="画像だけのZIP", interactive=False) with gr.Accordion("実行時間の記録", open=False): report = gr.File(label="計測結果 JSON", interactive=False) gr.Markdown("高速化の確認用です。画像ZIPには画像だけが入ります。") if USE_ANGLES: gr.Markdown("2.1用角度LoRAを併用した実験設定です。") keys.change(fn=allocation_message, inputs=keys, outputs=allocation, queue=False) keys.change(fn=pose_preview, inputs=keys, outputs=previews, queue=False) run.click(generate, [reference, keys, pose_enabled, resolution, extra, seed, random_seed, crop_close_views], [results, download, report, status, seed], concurrency_limit=1, concurrency_id="generation") if __name__ == "__main__": demo.queue(max_size=8, default_concurrency_limit=1).launch(max_file_size="20mb")