Fooocus/prompt_forge_v3_11.html

7711 lines
404 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PROMPT FORGE</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Noto+Sans+JP:wght@300;400;500&family=JetBrains+Mono:wght@400;600&display=swap');
:root {
--bg:#0a0a0f;--surface:#11111a;--surface2:#18182a;--border:#2a2a45;
--accent:#7c4dff;--accent2:#ff4da6;--accent3:#00e5ff;
--text:#e0e0ff;--text2:#8888bb;--success:#00e5a0;--neg:#ff6b6b;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{background:var(--bg);color:var(--text);font-family:'Noto Sans JP',sans-serif;min-height:100vh;padding:20px 14px;position:relative;overflow-x:hidden;
background-image:radial-gradient(ellipse 120% 80% at 20% 0%,rgba(124,77,255,.18) 0%,transparent 50%),
radial-gradient(ellipse 100% 60% at 85% 100%,rgba(255,77,166,.12) 0%,transparent 50%),
radial-gradient(ellipse 80% 50% at 50% 50%,rgba(0,229,255,.04) 0%,transparent 60%);}
body::before{content:'';position:fixed;inset:0;background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");pointer-events:none;z-index:0;}
body > *{position:relative;z-index:1;max-width:720px;margin-left:auto;margin-right:auto;}
@keyframes gradient-shift{0%,100%{background-position:0% 50%;}50%{background-position:100% 50%;}}
@keyframes title-glow{0%,100%{opacity:.6;}50%{opacity:1;}}
@keyframes subtitle-in{from{opacity:0;letter-spacing:.2em;}to{opacity:1;letter-spacing:1px;}}
h1{font-family:'Orbitron',monospace;font-size:18px;font-weight:900;letter-spacing:4px;text-align:center;margin-bottom:4px;
background:linear-gradient(110deg,var(--accent3) 0%,var(--accent) 35%,var(--accent2) 70%,var(--accent3) 100%);
background-size:200% auto;-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;
animation:gradient-shift 4s ease-in-out infinite;
filter:drop-shadow(0 0 20px rgba(124,77,255,.25)) drop-shadow(0 0 40px rgba(0,229,255,.15));}
h1::after{content:'';display:block;height:1px;width:60px;margin:6px auto 0;background:linear-gradient(90deg,transparent,var(--accent),transparent);opacity:.7;animation:title-glow 2.5s ease-in-out infinite;}
.subtitle{text-align:center;font-size:11px;color:var(--text2);letter-spacing:1px;margin-bottom:24px;animation:subtitle-in .8s ease-out both;}
/* API KEY */
.apikey-bar{display:flex;gap:8px;margin-bottom:16px;align-items:center;}
.apikey-input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);
font-family:'JetBrains Mono',monospace;font-size:11px;padding:9px 12px;outline:none;transition:border-color .2s;}
.apikey-input:focus{border-color:var(--accent3);box-shadow:0 0 0 3px rgba(0,229,255,.15);}
.apikey-input::placeholder{color:var(--text2);}
.apikey-status{font-size:9px;font-family:'Orbitron',monospace;letter-spacing:1px;white-space:nowrap;}
.apikey-status.ok{color:var(--success);}
.apikey-status.no{color:var(--neg);}
.card{background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:14px;margin-bottom:12px;position:relative;overflow:hidden;
transition:transform .25s ease, box-shadow .25s ease, border-color .25s ease;}
.card:hover{border-color:rgba(124,77,255,.35);box-shadow:0 8px 32px rgba(0,0,0,.35),0 0 0 1px rgba(124,77,255,.08);}
.card::before{content:'';position:absolute;top:0;left:0;right:0;height:3px;background:linear-gradient(90deg,var(--accent),var(--accent2));opacity:.75;}
.card.neg-card::before{background:linear-gradient(90deg,var(--neg),#ff9f43);}
.card.neg-card:hover{border-color:rgba(255,107,107,.35);box-shadow:0 8px 32px rgba(0,0,0,.35),0 0 0 1px rgba(255,107,107,.1);}
.card.final-card::before{background:linear-gradient(90deg,var(--success),var(--accent3));}
.card.final-card:hover{border-color:rgba(0,229,160,.35);box-shadow:0 8px 32px rgba(0,0,0,.35),0 0 0 1px rgba(0,229,160,.12);}
.card.save-card::before{background:linear-gradient(90deg,#f39c12,#e67e22);}
.card.save-card:hover{border-color:rgba(243,156,18,.4);box-shadow:0 8px 32px rgba(0,0,0,.35),0 0 0 1px rgba(243,156,18,.15);}
.card.key-card::before{background:linear-gradient(90deg,var(--accent3),var(--accent));}
.card.key-card:hover{border-color:rgba(0,229,255,.4);box-shadow:0 8px 32px rgba(0,0,0,.35),0 0 0 1px rgba(0,229,255,.12);}
.card.wc-shelf-card:hover{border-color:rgba(255,193,7,.5);box-shadow:0 8px 32px rgba(0,0,0,.35),0 0 0 1px rgba(255,193,7,.2);}
.slabel{font-family:'Orbitron',monospace;font-size:9px;letter-spacing:2px;color:var(--accent);margin-bottom:8px;text-transform:uppercase;display:flex;align-items:center;gap:6px;}
.slabel.neg{color:var(--neg);} .slabel.final{color:var(--success);} .slabel.save-col{color:#f39c12;} .slabel.key-col{color:var(--accent3);}
textarea{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);
font-family:'Noto Sans JP',sans-serif;font-size:13px;padding:11px;resize:vertical;min-height:80px;outline:none;transition:border-color .2s;}
textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px rgba(124,77,255,.12);}
textarea.neg-ta:focus{border-color:var(--neg);box-shadow:0 0 0 3px rgba(255,107,107,.12);}
textarea::placeholder{color:var(--text2);}
.output-area{background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:#00e5ff;
font-family:'JetBrains Mono',monospace;font-size:11px;padding:11px;min-height:50px;white-space:pre-wrap;word-break:break-all;line-height:1.8;}
.output-area.empty{color:var(--text2);font-family:'Noto Sans JP',sans-serif;font-size:12px;}
.output-area.neg-color{color:#ff9f9f;}
.btn-translate{width:100%;padding:12px;background:linear-gradient(135deg,var(--accent),#5c35cc);border:none;border-radius:9px;color:#fff;
font-family:'Orbitron',monospace;font-size:10px;font-weight:700;letter-spacing:2px;cursor:pointer;margin:9px 0;transition:all .2s;}
.btn-translate:hover{transform:translateY(-2px);filter:brightness(1.15);box-shadow:0 6px 20px rgba(124,77,255,.3);}
.btn-translate:active{transform:translateY(0);}
.btn-translate:disabled{opacity:.5;cursor:not-allowed;transform:none;}
.btn-translate.neg-btn{background:linear-gradient(135deg,#c0392b,#e74c3c);}
.spinner{display:inline-block;width:12px;height:12px;border:2px solid rgba(255,255,255,.3);border-top-color:#fff;border-radius:50%;animation:spin .8s linear infinite;vertical-align:middle;margin-right:6px;}
@keyframes spin{to{transform:rotate(360deg);}}
.row-btns{display:flex;gap:6px;align-items:center;margin-top:8px;flex-wrap:wrap;}
.btn{padding:5px 11px;border-radius:6px;font-size:9px;cursor:pointer;font-family:'Orbitron',monospace;letter-spacing:1px;transition:all .2s;border:1px solid var(--accent);background:rgba(124,77,255,.1);color:var(--accent);}
.btn:hover{background:rgba(124,77,255,.25);transform:translateY(-1px);}
.btn:active{transform:translateY(0);}
.btn.btn-c{border-color:var(--accent3);background:var(--surface2);color:var(--accent3);}
.btn.btn-c:hover{background:rgba(0,229,255,.1);}
.btn.btn-c.copied{border-color:var(--success);color:var(--success);}
.btn.btn-d{border-color:#ff6666;background:transparent;color:#ff6666;}
.btn.btn-d:hover{background:rgba(255,100,100,.1);}
.btn.btn-save{border-color:#f39c12;background:rgba(243,156,18,.1);color:#f39c12;}
.btn.btn-save:hover{background:rgba(243,156,18,.25);}
.btn.btn-rand{border-color:#00e5a0;background:rgba(0,229,160,.1);color:#00e5a0;}
.btn.btn-rand:hover{background:rgba(0,229,160,.25);}
/* ── キャラクタープリセット ── */
.cp-grid{display:flex;flex-wrap:wrap;gap:5px;min-height:22px;}
.cp-btn{padding:5px 11px;border-radius:20px;border:1px solid rgba(255,215,0,.35);
background:rgba(255,215,0,.06);color:#ffe57f;font-size:11px;cursor:pointer;
font-family:'Noto Sans JP',sans-serif;transition:all .18s;white-space:nowrap;user-select:none;}
.cp-btn:hover{border-color:#ffd740;background:rgba(255,215,0,.18);transform:translateY(-1px);box-shadow:0 3px 10px rgba(0,0,0,.25);}
.cp-btn.cp-active{border-color:#ffd740;background:rgba(255,215,0,.22);color:#fff;box-shadow:0 0 0 2px rgba(255,215,0,.3);}
.cp-save-row{display:flex;gap:6px;align-items:center;padding-top:8px;border-top:1px solid var(--border);margin-top:8px;}
.preset-tabs{display:flex;gap:5px;margin-bottom:10px;flex-wrap:wrap;align-items:center;}
.ptab{padding:4px 12px;border-radius:20px;border:1px solid var(--border);background:transparent;color:var(--text2);font-size:10px;cursor:pointer;transition:all .2s;font-family:'Noto Sans JP',sans-serif;}
.ptab.active{border-color:var(--accent2);color:var(--accent2);background:rgba(255,77,166,.08);}
.ptab-animagine.active{border-color:#7c4dff;color:#a78bfa;background:rgba(124,77,255,.12);}
.ptab-pony.active{border-color:#ff4da6;color:#ff80c0;background:rgba(255,77,166,.12);}
.ptab-general.active{border-color:#00e5ff;color:#00e5ff;background:rgba(0,229,255,.08);}
.ptab-style.active{border-color:#00e5a0;color:#00e5a0;background:rgba(0,229,160,.08);}
.ptab-rating{border-color:rgba(255,230,0,.3);color:rgba(255,230,0,.7);}
.ptab-rating.active{border-color:#ffe600;color:#ffe600;background:rgba(255,230,0,.08);}
.ptab-year{border-color:rgba(0,229,255,.25);color:rgba(0,229,255,.6);}
.ptab-year.active{border-color:#00e5ff;color:#00e5ff;background:rgba(0,229,255,.08);}
.src-rating{background:rgba(255,230,0,.18);border-color:rgba(255,230,0,.4);color:#ffe600;}
.ptab-chara.active{border-color:#f9a825;color:#ffe57f;background:rgba(249,168,37,.08);}
.ptab-appear.active{border-color:#ab47bc;color:#e040fb;background:rgba(171,71,188,.08);}
.ptab-costume{border-color:rgba(255,183,77,.3);color:rgba(255,183,77,.7);}
.ptab-costume.active{border-color:#ffb74d;color:#ffe0b2;background:rgba(255,183,77,.1);}
.src-costume{background:rgba(255,183,77,.12);border-color:rgba(255,183,77,.4);color:#ffe0b2;}
.ptab-nsfw.active{border-color:#ff6b6b;color:#ff9f9f;background:rgba(255,107,107,.12);}
.ptab-neg-ani{border-color:var(--border);color:var(--text2);}
.ptab-neg-ani.active{border-color:#ff7043;color:#ffab91;background:rgba(255,112,67,.12);}
.ptab-skin-ctrl{border-color:rgba(255,183,77,.3);color:rgba(255,183,77,.7);}
.ptab-skin-ctrl.active{border-color:#ffb74d;color:#ffe0b2;background:rgba(255,183,77,.22);}
.ptab-neg-pony{border-color:var(--border);color:var(--text2);}
.ptab-neg-pony.active{border-color:#f06292;color:#f8bbd0;background:rgba(240,98,146,.12);}
/* negative selected zone */
.neg-sel-zone{display:flex;flex-wrap:wrap;gap:5px;min-height:36px;padding:8px;background:var(--surface2);border-radius:6px;border:1px dashed rgba(255,107,107,.3);margin-top:8px;}
.neg-sel-label{font-family:'Orbitron',monospace;font-size:9px;letter-spacing:2px;color:var(--neg);margin-top:10px;margin-bottom:4px;display:flex;align-items:center;gap:6px;}
.all-btn{margin-left:auto;padding:4px 10px;border-radius:20px;border:1px solid var(--accent3);background:transparent;color:var(--accent3);
font-size:9px;cursor:pointer;transition:all .2s;font-family:'Orbitron',monospace;letter-spacing:1px;white-space:nowrap;}
.all-btn:hover{background:rgba(0,229,255,.1);}
.all-btn.all-on{border-color:var(--accent2);color:var(--accent2);background:rgba(255,77,166,.1);}
.search-wrap{position:relative;margin-bottom:10px;}
.search-input{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:8px;color:var(--text);
font-family:'JetBrains Mono',monospace;font-size:11px;padding:8px 32px 8px 10px;outline:none;transition:border-color .2s;}
.search-input:focus{border-color:var(--accent3);box-shadow:0 0 0 3px rgba(0,229,255,.12);}
.search-input::placeholder{color:var(--text2);}
.search-clear{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;color:var(--text2);cursor:pointer;font-size:14px;padding:0;}
.search-clear:hover{color:var(--text);}
.tags-grid{display:flex;flex-wrap:wrap;gap:5px;}
.tag{padding:4px 9px;border-radius:6px;border:1px solid var(--border);background:var(--surface2);color:var(--text2);
font-family:'JetBrains Mono',monospace;font-size:10px;cursor:pointer;transition:all .18s;user-select:none;}
.tag:hover{border-color:var(--accent);color:var(--text);transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,.2);}
.tag.selected{border-color:var(--accent2);background:rgba(255,77,166,.15);color:var(--accent2);}
.tag.search-match{border-color:var(--accent3);color:var(--accent3);}
.tag.search-match.selected{border-color:var(--accent2);color:var(--accent2);}
.tag-jp{display:block;font-size:8px;color:var(--text2);font-family:'Noto Sans JP',sans-serif;margin-top:2px;line-height:1.2;}
.tag.selected .tag-jp{color:rgba(255,77,166,.7);}
.tag:hover .tag-jp{color:var(--text2);}
.selected-tags-zone{display:flex;flex-wrap:wrap;gap:5px;min-height:36px;padding:8px;background:var(--surface2);border-radius:6px;border:1px dashed var(--border);margin-top:8px;}
.sel-tag{padding:4px 8px;border-radius:5px;font-family:'JetBrains Mono',monospace;font-size:10px;cursor:grab;user-select:none;display:flex;align-items:center;gap:5px;transition:all .15s;border:1px solid var(--accent2);background:rgba(255,77,166,.12);color:var(--accent2);}
.sel-tag.src-animagine{border-color:#7c4dff;background:rgba(124,77,255,.18);color:#a78bfa;}
.sel-tag.src-pony{border-color:#e040a0;background:rgba(224,64,160,.15);color:#ff80c0;}
.sel-tag.src-general{border-color:#00b8cc;background:rgba(0,184,204,.12);color:#00e5ff;}
.sel-tag.src-style{border-color:#00c896;background:rgba(0,200,150,.12);color:#00e5a0;}
.zgl-style{color:#00e5a0;border:1px solid rgba(0,229,160,.4);background:rgba(0,229,160,.08);}
.sel-tag.src-nsfw{border-color:#cc4444;background:rgba(204,68,68,.12);color:#ff9f9f;}
.sel-tag.src-neg_animagine{border-color:#ff7043;background:rgba(255,112,67,.12);color:#ffab91;}
.sel-tag.src-neg_pony{border-color:#f06292;background:rgba(240,98,146,.12);color:#f8bbd0;}
.sel-tag.src-chara{border-color:#f9a825;background:rgba(249,168,37,.12);color:#ffe57f;}
.sel-tag.src-appear{border-color:#ab47bc;background:rgba(171,71,188,.12);color:#e040fb;}
.sel-tag.locked{box-shadow:0 0 0 1px #fff3,inset 0 0 0 1px #fff1;}
.lock-btn{cursor:pointer;font-size:10px;line-height:1;opacity:.45;transition:opacity .15s;padding:0 2px;}
.lock-btn:hover{opacity:1;}
.lock-btn.is-locked{opacity:1;}
.sel-tag:active{cursor:grabbing;}
.sel-tag.dragging{opacity:.4;border-style:dashed;}
.sel-tag.drag-over{border-color:#fff;background:rgba(255,255,255,.1);}
.sel-tag .del-tag{cursor:pointer;color:var(--text2);font-size:12px;line-height:1;}
.sel-tag .del-tag:hover{color:var(--neg);}
.empty-zone{color:var(--text2);font-size:11px;font-family:'Noto Sans JP',sans-serif;padding:2px;}
.tag-count{font-size:9px;color:var(--text2);font-family:'JetBrains Mono',monospace;margin-left:auto;}
.zone-group-label{font-family:'Orbitron',monospace;font-size:7px;letter-spacing:1px;padding:1px 6px;border-radius:3px;margin-right:2px;flex-shrink:0;}
.zgl-animagine{color:#a78bfa;border:1px solid rgba(124,77,255,.4);background:rgba(124,77,255,.1);}
.zgl-pony{color:#ff80c0;border:1px solid rgba(255,77,166,.4);background:rgba(255,77,166,.1);}
.zgl-general{color:#00e5ff;border:1px solid rgba(0,229,255,.3);background:rgba(0,229,255,.08);}
.zgl-chara{color:#ffe57f;border:1px solid rgba(249,168,37,.4);background:rgba(249,168,37,.08);}
.zgl-appear{color:#e040fb;border:1px solid rgba(171,71,188,.4);background:rgba(171,71,188,.08);}
.zgl-nsfw{color:#ff9f9f;border:1px solid rgba(255,107,107,.4);background:rgba(255,107,107,.1);}
.zgl-neg_animagine{color:#ffab91;border:1px solid rgba(255,112,67,.4);background:rgba(255,112,67,.1);}
.zgl-neg_pony{color:#f8bbd0;border:1px solid rgba(240,98,146,.4);background:rgba(240,98,146,.1);}
.zone-sep{width:100%;height:1px;background:rgba(255,255,255,.06);flex-basis:100%;margin:2px 0;}
/* translated text display in selzone */
.sel-zone-translated{width:100%;background:rgba(0,229,255,.06);border:1px dashed rgba(0,229,255,.2);border-radius:6px;padding:7px 10px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--accent3);line-height:1.7;word-break:break-all;margin-bottom:4px;}
.sel-zone-translated-label{font-family:'Orbitron',monospace;font-size:7px;letter-spacing:1px;color:var(--accent3);opacity:.7;display:block;margin-bottom:3px;}
.random-result{margin-top:8px;padding:8px 10px;background:var(--surface2);border-radius:6px;border:1px dashed rgba(0,229,160,.3);
font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--success);word-break:break-all;min-height:32px;line-height:1.7;}
.save-name-row{display:flex;gap:7px;margin-bottom:8px;}
.save-name-input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:7px;color:var(--text);
font-family:'Noto Sans JP',sans-serif;font-size:12px;padding:7px 10px;outline:none;transition:border-color .2s;}
.save-name-input:focus{border-color:#f39c12;box-shadow:0 0 0 3px rgba(243,156,18,.15);}
.save-name-input::placeholder{color:var(--text2);}
.save-list{display:flex;flex-direction:column;gap:5px;max-height:220px;overflow-y:auto;}
.save-item{padding:8px 10px;background:var(--surface2);border-radius:7px;border:1px solid var(--border);transition:all .15s;}
.save-item:hover{border-color:#f39c12;}
.save-item-name{font-family:'Orbitron',monospace;font-size:9px;color:#f39c12;margin-bottom:3px;letter-spacing:1px;}
.save-item-pos{font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--success);word-break:break-all;line-height:1.5;margin-bottom:2px;}
.save-item-neg{font-family:'JetBrains Mono',monospace;font-size:10px;color:#ff9f9f;word-break:break-all;line-height:1.5;}
.save-item-row{display:flex;align-items:center;gap:6px;margin-top:5px;}
.empty-saves{color:var(--text2);font-size:12px;padding:8px;text-align:center;}
.coll-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none;}
.coll-toggle .arr{font-size:9px;transition:transform .2s;color:var(--text2);}
.coll-toggle.open .arr{transform:rotate(90deg);}
.coll-body{overflow:hidden;transition:max-height .35s ease;}
.coll-body.closed{max-height:0;}
.coll-body.open{max-height:2000px;padding-top:10px;}
.final-output{font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--success);background:var(--surface2);
border:1px solid rgba(0,229,160,.3);border-radius:8px;padding:11px;min-height:44px;word-break:break-all;line-height:1.8;}
.final-neg-output{font-family:'JetBrains Mono',monospace;font-size:11px;color:#ff9f9f;background:var(--surface2);
border:1px solid rgba(255,107,107,.3);border-radius:8px;padding:11px;min-height:36px;word-break:break-all;line-height:1.8;margin-top:8px;}
.label-s{font-size:9px;font-family:'Orbitron',monospace;letter-spacing:1px;color:var(--text2);margin-bottom:4px;}
.divider{height:1px;background:var(--border);margin:10px 0;}
::-webkit-scrollbar{width:6px;}
::-webkit-scrollbar-track{background:var(--surface2);border-radius:3px;}
::-webkit-scrollbar-thumb{background:linear-gradient(180deg,var(--border),rgba(124,77,255,.3));border-radius:3px;}
::-webkit-scrollbar-thumb:hover{background:var(--accent);}
.api-note{font-size:10px;color:var(--text2);text-align:center;margin-top:14px;line-height:1.6;}
.err-msg{color:var(--neg);font-size:11px;font-family:'Noto Sans JP',sans-serif;margin-top:6px;}
/* ===== WC BADGE (タブに密着するwildcardバッジ) ===== */
.wc-btn {
display: inline-flex;
align-items: center;
gap: 2px;
background: transparent;
border: 1px solid rgba(255,255,255,.1);
border-radius: 3px;
color: rgba(255,255,255,.22);
font-family: 'JetBrains Mono', monospace;
font-size: 7.5px;
letter-spacing: .5px;
padding: 2px 5px;
cursor: pointer;
margin-left: -3px;
transition: all .18s;
flex-shrink: 0;
line-height: 1;
}
.wc-btn::before {
content: '◆';
font-size: 5px;
opacity: .5;
}
.wc-btn:hover {
background: rgba(255,193,7,.14);
border-color: rgba(255,193,7,.55);
color: #ffd54f;
}
.wc-btn:hover::before { opacity: 1; }
.wc-btn.wc-active {
background: rgba(255,193,7,.22);
border-color: rgba(255,193,7,.75);
color: #ffd54f;
box-shadow: 0 0 6px rgba(255,193,7,.25);
}
.wc-btn.wc-active::before { opacity: 1; content: '◆'; color: #ffd54f; }
.wc-all-btn {
padding: 3px 10px;
border-radius: 20px;
border: 1px solid rgba(255,193,7,.45);
background: transparent;
color: rgba(255,193,7,.8);
font-family: 'Orbitron', monospace;
font-size: 8px;
letter-spacing: 1px;
cursor: pointer;
transition: all .18s;
white-space: nowrap;
}
.wc-all-btn:hover {
background: rgba(255,193,7,.15);
border-color: rgba(255,193,7,.8);
color: #ffd54f;
}
.wc-all-btn.wc-all-off {
border-color: rgba(255,100,100,.4);
color: rgba(255,120,120,.7);
}
.wc-all-btn.wc-all-off:hover {
background: rgba(255,100,100,.12);
border-color: rgba(255,100,100,.7);
color: #ff9f9f;
}
/* ===== WILDCARD SHELF BUTTONS ===== */
.wc-shelf-btn {
display: flex;
align-items: center;
gap: 7px;
background: rgba(255,193,7,.04);
border: 1px solid rgba(255,193,7,.18);
border-radius: 9px;
padding: 8px 14px 8px 10px;
cursor: pointer;
transition: all .18s;
color: var(--text);
text-align: left;
}
.wc-shelf-btn:hover {
background: rgba(255,193,7,.13);
border-color: rgba(255,193,7,.5);
transform: translateY(-1px);
box-shadow: 0 4px 14px rgba(255,193,7,.08);
}
.wc-shelf-btn.wc-active {
background: rgba(255,193,7,.18);
border-color: rgba(255,193,7,.7);
box-shadow: 0 0 12px rgba(255,193,7,.18);
}
.wc-shelf-btn.wc-active .wcsb-tag {
color: rgba(255,193,7,.75);
}
.wc-shelf-btn.wc-active .wcsb-label {
text-shadow: 0 0 8px rgba(255,193,7,.4);
}
.wcsb-icon {
font-size: 16px;
line-height: 1;
flex-shrink: 0;
}
.wcsb-text {
display: flex;
flex-direction: column;
gap: 2px;
}
.wcsb-label {
font-family: 'Orbitron', monospace;
font-size: 9px;
letter-spacing: 1px;
color: #ffd54f;
white-space: nowrap;
}
.wcsb-tag {
font-family: 'JetBrains Mono', monospace;
font-size: 8px;
color: rgba(255,193,7,.38);
white-space: nowrap;
}
/* ===== CHARA SEARCH ===== */
.chara-search-input {
background: rgba(30,12,48,.9);
border: 1px solid rgba(249,168,37,.25);
border-radius: 5px;
color: #ffe57f;
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
padding: 3px 7px;
width: 130px;
outline: none;
transition: border-color .2s;
}
.chara-search-input:focus { border-color: rgba(249,168,37,.65); }
.chara-search-input::placeholder { color: rgba(255,229,127,.3); }
/* ===== WC TOAST ===== */
.wc-toast {
position: fixed;
bottom: 28px;
left: 50%;
transform: translateX(-50%) translateY(8px);
background: rgba(30,20,8,.95);
border: 1px solid rgba(255,193,7,.45);
color: #ffd54f;
font-family: 'JetBrains Mono', monospace;
font-size: 11px;
padding: 8px 18px;
border-radius: 20px;
pointer-events: none;
opacity: 0;
transition: opacity .25s, transform .25s;
z-index: 9999;
white-space: nowrap;
letter-spacing: .5px;
}
.wc-toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ===== COLLAPSE TOGGLE ===== */
.coll-toggle { justify-content: space-between; }
.coll-toggle .arr { font-size: 10px; margin-left: auto; flex-shrink:0; }
/* ===== PAGE NAV ===== */
.page-nav { display:flex; gap:6px; margin-bottom:16px; flex-wrap:wrap; }
.page-nav-btn { padding:8px 16px; border-radius:10px; border:1px solid var(--border); background:var(--surface2); color:var(--text2); font-size:11px; font-family:'Noto Sans JP',sans-serif; cursor:pointer; transition:all .2s; }
.page-nav-btn:hover { border-color:var(--accent); color:var(--text); background:rgba(124,77,255,.08); }
.page-nav-btn.active { border-color:var(--accent); color:var(--accent); background:rgba(124,77,255,.15); font-weight:500; }
.page-container { display:none; }
.page-container.active { display:block; }
/* ===== SUB-MODE TOGGLE (イラスト / 実写) ===== */
.sub-mode-bar { display:flex; gap:8px; margin-bottom:12px; }
.sub-mode-btn {
flex:1; padding:8px 12px; border-radius:10px;
border:1px solid var(--border); background:var(--surface2);
color:var(--text2); font-family:'Orbitron',monospace; font-size:10px;
letter-spacing:1px; cursor:pointer; transition:all .2s; }
.sub-mode-btn:hover { border-color:var(--accent3); color:var(--text); }
.sub-mode-btn.active { border-color:var(--accent3); color:var(--accent3); background:rgba(0,229,255,.1); font-weight:600; }
/* 実写モード切り替え */
#page1:not(.page1-photo) .photo-only { display:none !important; }
#page1.page1-photo .illust-only { display:none !important; }
/* ===== FOOOCUS 画像生成 UI ===== */
/* 生成ページ専用カード */
.card.gen-card::before { background: linear-gradient(90deg, #00e5ff, #7c4dff); }
.card.gen-card:hover { border-color: rgba(0,229,255,.4); box-shadow: 0 8px 32px rgba(0,0,0,.35), 0 0 0 1px rgba(0,229,255,.12); }
.card.gallery-card::before { background: linear-gradient(90deg, #ff4da6, #7c4dff); }
.card.gallery-card:hover { border-color: rgba(255,77,166,.35); box-shadow: 0 8px 32px rgba(0,0,0,.35), 0 0 0 1px rgba(255,77,166,.1); }
/* 生成ボタンメインCTA */
.btn-generate {
width: 100%; padding: 14px; border: none; border-radius: 11px; cursor: pointer;
font-family: 'Orbitron', monospace; font-size: 12px; font-weight: 900;
letter-spacing: 3px; color: #fff; position: relative; overflow: hidden;
background: linear-gradient(135deg, #7c4dff 0%, #ff4da6 50%, #00e5ff 100%);
background-size: 200% auto; transition: all .3s;
box-shadow: 0 4px 20px rgba(124,77,255,.35);
animation: gen-btn-shift 3s ease-in-out infinite;
}
@keyframes gen-btn-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.btn-generate:hover { transform: translateY(-2px); filter: brightness(1.15); box-shadow: 0 8px 30px rgba(124,77,255,.5); }
.btn-generate:active { transform: translateY(0); }
.btn-generate:disabled { opacity: .5; cursor: not-allowed; transform: none; animation: none; background: linear-gradient(135deg, #333, #444); }
/* ミニ版生成ボタン(最終プロンプトカードの行に置く) */
.btn-gen-mini {
padding: 5px 13px; border-radius: 6px; font-size: 9px; cursor: pointer;
font-family: 'Orbitron', monospace; letter-spacing: 1px; transition: all .2s;
border: 1px solid var(--accent3); background: rgba(0,229,255,.1); color: var(--accent3);
}
.btn-gen-mini:hover { background: rgba(0,229,255,.22); transform: translateY(-1px); box-shadow: 0 3px 10px rgba(0,229,255,.2); }
.btn-gen-mini:disabled { opacity: .5; cursor: not-allowed; transform: none; }
/* APIステータスインジケーター */
.fooocus-status { font-size: 9px; font-family: 'Orbitron', monospace; letter-spacing: 1px; white-space: nowrap; }
.fooocus-status.online { color: var(--success); }
.fooocus-status.offline { color: var(--neg); }
.fooocus-status.testing { color: #ffd54f; }
.status-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; margin-right: 4px; vertical-align: middle; }
.status-dot.online { background: var(--success); box-shadow: 0 0 6px var(--success); }
.status-dot.offline { background: var(--neg); }
.status-dot.testing { background: #ffd54f; animation: blink .7s ease-in-out infinite; }
@keyframes blink { 0%,100%{opacity:1;} 50%{opacity:.3;} }
/* パラメータ選択行 */
.gen-param-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 10px; }
.gen-param-label { font-family: 'Orbitron', monospace; font-size: 8px; letter-spacing: 1px; color: var(--text2); white-space: nowrap; }
.gen-select {
background: var(--surface2); border: 1px solid var(--border); border-radius: 7px;
color: var(--text); font-family: 'JetBrains Mono', monospace; font-size: 11px;
padding: 6px 10px; outline: none; cursor: pointer; transition: border-color .2s;
}
.gen-select:focus { border-color: var(--accent3); box-shadow: 0 0 0 3px rgba(0,229,255,.12); }
/* ローディングUI */
.gen-loading {
display: none; margin: 12px 0;
padding: 14px; background: rgba(0,229,255,.04); border: 1px solid rgba(0,229,255,.2);
border-radius: 10px; text-align: center;
}
.gen-loading.active { display: block; }
.gen-loading-text {
font-family: 'Orbitron', monospace; font-size: 10px; letter-spacing: 2px;
color: var(--accent3); margin-bottom: 10px;
animation: gen-pulse 1.5s ease-in-out infinite;
}
@keyframes gen-pulse { 0%,100%{opacity:1;} 50%{opacity:.4;} }
.gen-progress-bar {
height: 4px; border-radius: 2px; background: var(--border); overflow: hidden; margin: 6px 0;
}
.gen-progress-fill {
height: 100%; border-radius: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent3));
animation: gen-progress 2s ease-in-out infinite;
width: 100%;
}
@keyframes gen-progress {
0% { transform: translateX(-100%) scaleX(.5); }
50% { transform: translateX(0%) scaleX(1); }
100% { transform: translateX(100%) scaleX(.5); }
}
.gen-elapsed { font-family: 'JetBrains Mono', monospace; font-size: 9px; color: var(--text2); }
/* エラーメッセージ */
.gen-error {
display: none; margin-top: 10px; padding: 10px 14px;
background: rgba(255,107,107,.08); border: 1px solid rgba(255,107,107,.35);
border-radius: 8px; color: var(--neg); font-size: 11px; line-height: 1.6;
font-family: 'Noto Sans JP', sans-serif;
}
.gen-error.active { display: block; }
.gen-error-title { font-family: 'Orbitron', monospace; font-size: 9px; letter-spacing: 1px; margin-bottom: 4px; }
/* ギャラリー */
.gen-gallery {
display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; min-height: 0;
}
.gen-gallery-item {
position: relative; border-radius: 10px; overflow: hidden;
border: 1px solid var(--border); transition: all .25s; cursor: pointer;
background: var(--surface2);
}
.gen-gallery-item:hover { border-color: var(--accent2); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,.4); }
.gen-gallery-item img { display: block; width: 100%; height: auto; border-radius: 10px; }
.gen-gallery-item .gen-img-actions {
position: absolute; bottom: 0; left: 0; right: 0; padding: 6px 5px;
display: flex; flex-wrap: wrap; gap: 3px;
background: linear-gradient(transparent, rgba(0,0,0,.82)); opacity: 0; transition: opacity .2s;
}
.gen-gallery-item:hover .gen-img-actions { opacity: 1; }
.gen-img-btn {
flex: 1 1 calc(33% - 3px); min-width: 0; padding: 4px 3px; border-radius: 5px; font-size: 8px; cursor: pointer;
font-family: 'Orbitron', monospace; letter-spacing: .3px; border: 1px solid rgba(255,255,255,.3);
background: rgba(0,0,0,.55); color: #fff; transition: all .15s; text-align: center; white-space: nowrap;
}
.gen-img-btn:hover { background: rgba(255,255,255,.2); border-color: rgba(255,255,255,.6); }
.gen-img-btn.save-to-memo { border-color: rgba(243,156,18,.6); color: #f39c12; }
.gen-img-btn.save-to-memo:hover { background: rgba(243,156,18,.2); }
.gen-gallery-empty {
width: 100%; padding: 24px; text-align: center; color: var(--text2);
font-family: 'Noto Sans JP', sans-serif; font-size: 12px; border-radius: 8px;
border: 1px dashed var(--border); background: rgba(255,255,255,.01);
}
/* 画像拡大モーダル */
.gen-img-modal {
display: none; position: fixed; inset: 0; z-index: 10000;
background: rgba(0,0,0,.88); align-items: center; justify-content: center;
cursor: pointer;
}
.gen-img-modal.active { display: flex; }
.gen-img-modal img {
max-width: 92vw; max-height: 92vh; border-radius: 12px;
box-shadow: 0 0 60px rgba(0,0,0,.8), 0 0 0 1px rgba(255,255,255,.08);
}
.gen-img-modal-close {
position: absolute; top: 18px; right: 22px;
color: rgba(255,255,255,.7); font-size: 28px; cursor: pointer; line-height: 1;
transition: color .2s; font-family: 'Orbitron', monospace;
}
.gen-img-modal-close:hover { color: #fff; }
/* ===== 色調補正モーダル ===== */
.color-modal-overlay {
display: none; position: fixed; inset: 0; z-index: 9000;
background: rgba(0,0,0,.87); align-items: center; justify-content: center;
}
.color-modal-overlay.active { display: flex; }
.color-modal {
background: var(--surface); border: 1px solid rgba(0,229,255,.35);
border-radius: 16px; padding: 20px; width: min(520px, 95vw);
max-height: 90vh; overflow-y: auto; position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,.7), 0 0 0 1px rgba(0,229,255,.12);
}
.color-modal-title {
font-family: 'Orbitron', monospace; font-size: 10px; letter-spacing: 3px;
color: var(--accent3); margin-bottom: 14px; text-align: center;
}
.color-preview-img {
width: 100%; border-radius: 8px; overflow: hidden; border: 1px solid var(--border); margin-bottom: 12px;
}
.color-preview-img img { width: 100%; display: block; border-radius: 8px; transition: filter .05s; }
.color-slider-group { margin-bottom: 10px; }
.color-slider-label {
display: flex; justify-content: space-between; align-items: center;
font-family: 'Noto Sans JP', sans-serif; font-size: 11px; color: var(--text2); margin-bottom: 4px;
}
.color-slider-val { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--accent3); min-width: 40px; text-align: right; }
.color-slider {
width: 100%; height: 8px; -webkit-appearance: none; appearance: none;
border-radius: 4px; outline: none; cursor: pointer;
}
.color-slider::-webkit-slider-thumb {
-webkit-appearance: none; width: 20px; height: 20px; border-radius: 50%;
background: var(--accent3); cursor: pointer; box-shadow: 0 0 8px rgba(0,229,255,.5);
}
.color-slider.brightness-sl { background: linear-gradient(90deg, #111, #fff); }
.color-slider.contrast-sl { background: linear-gradient(90deg, #888, #000 40%, #fff); }
.color-slider.saturation-sl { background: linear-gradient(90deg, #777, #e040fb); }
.color-slider.hue-sl { background: linear-gradient(90deg, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); }
.color-slider.sepia-sl { background: linear-gradient(90deg, #fff, #c8a06e); }
.color-presets { display: flex; gap: 5px; flex-wrap: wrap; margin-bottom: 12px; }
.color-preset-btn {
padding: 5px 10px; border-radius: 20px; border: 1px solid var(--border);
background: var(--surface2); color: var(--text2); font-size: 10px; cursor: pointer;
transition: all .18s; font-family: 'Noto Sans JP', sans-serif;
}
.color-preset-btn:hover { border-color: var(--accent3); color: var(--accent3); background: rgba(0,229,255,.06); }
.color-preset-btn.active { border-color: var(--accent3); color: var(--accent3); background: rgba(0,229,255,.12); }
.color-modal-footer { display: flex; gap: 8px; margin-top: 14px; flex-wrap: wrap; }
.color-modal-close, .x-modal-close {
position: absolute; top: 14px; right: 16px; background: none; border: none;
color: var(--text2); font-size: 20px; cursor: pointer; padding: 0; line-height: 1;
}
.color-modal-close:hover, .x-modal-close:hover { color: #fff; }
.posted-badge {
position: absolute; top: 6px; right: 6px;
background: rgba(0,0,0,.75); border: 1px solid rgba(29,161,242,.7);
color: #1da1f2; font-size: 8px; border-radius: 20px;
padding: 2px 7px; font-family: 'Orbitron', monospace; letter-spacing: 1px;
pointer-events: none; z-index: 2;
}
/* ===== X投稿モーダル ===== */
.x-modal-overlay {
display: none; position: fixed; inset: 0; z-index: 9001;
background: rgba(0,0,0,.9); align-items: center; justify-content: center;
}
.x-modal-overlay.active { display: flex; }
.x-modal {
background: var(--surface); border: 1px solid rgba(29,161,242,.4);
border-radius: 16px; padding: 20px; width: min(460px, 95vw);
max-height: 90vh; overflow-y: auto; position: relative;
box-shadow: 0 20px 60px rgba(0,0,0,.7), 0 0 0 1px rgba(29,161,242,.15);
}
.x-modal-title {
font-family: 'Orbitron', monospace; font-size: 11px; letter-spacing: 3px;
color: #1da1f2; margin-bottom: 14px; text-align: center;
}
.x-preview-thumb { width: 100%; border-radius: 8px; margin-bottom: 12px; border: 1px solid var(--border); display: block; }
.x-caption-area {
width: 100%; background: var(--surface2); border: 1px solid var(--border);
border-radius: 8px; color: var(--text); font-family: 'Noto Sans JP', sans-serif;
font-size: 13px; padding: 10px; resize: vertical; min-height: 80px; outline: none;
transition: border-color .2s;
}
.x-caption-area:focus { border-color: #1da1f2; box-shadow: 0 0 0 3px rgba(29,161,242,.12); }
.x-char-count { text-align: right; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--text2); margin-top: 4px; }
.x-char-count.over { color: var(--neg); font-weight: bold; }
.x-hashtag-row { display: flex; flex-wrap: wrap; gap: 5px; margin: 8px 0; }
.x-hashtag-btn {
padding: 4px 10px; border-radius: 20px; border: 1px solid rgba(29,161,242,.35);
background: rgba(29,161,242,.05); color: rgba(29,161,242,.8); font-size: 10px;
cursor: pointer; transition: all .15s; font-family: 'JetBrains Mono', monospace;
}
.x-hashtag-btn:hover { border-color: #1da1f2; background: rgba(29,161,242,.15); color: #1da1f2; }
.x-hashtag-btn.selected { border-color: #1da1f2; background: rgba(29,161,242,.22); color: #fff; }
.x-info-box {
background: rgba(29,161,242,.06); border: 1px solid rgba(29,161,242,.2);
border-radius: 8px; padding: 10px; margin-top: 10px;
}
</style>
</head>
<body>
<h1>PROMPT FORGE</h1>
<p class="subtitle">Fooocus 日本語プロンプト → 英語変換 v3</p>
<!-- GLOBAL FINAL PROMPT (常に表示) -->
<div class="card final-card">
<div class="coll-toggle open" onclick="toggleColl(this,'finalBody')">
<div class="slabel final" style="margin-bottom:0;">▸ 最終プロンプト</div>
<span class="arr"></span>
</div>
<div class="coll-body open" id="finalBody">
<div class="label-s">POSITIVE</div>
<div class="final-output" id="finalOutput">ここにポジティブプロンプトが表示されます</div>
<div class="label-s" style="margin-top:8px;">NEGATIVE</div>
<div class="final-neg-output" id="finalNegOutput">ここにネガティブプロンプトが表示されます</div>
<div class="row-btns" style="margin-top:9px;">
<button class="btn btn-c" id="cpFPos" onclick="copyBtn(getFinalPos(),'cpFPos','✦ COPY POS')">✦ COPY POS</button>
<button class="btn btn-c" style="border-color:#ff9f9f;color:#ff9f9f;" id="cpFNeg" onclick="copyBtn(getFinalNeg(),'cpFNeg','✦ COPY NEG')">✦ COPY NEG</button>
<button class="btn btn-d" onclick="resetAll()">RESET</button>
<button class="btn-gen-mini" id="genMinBtn" onclick="generateImage()" title="現在のプロンプトでFooocusに画像生成リクエスト">🚀 GENERATE</button>
</div>
</div>
</div>
<nav class="page-nav" aria-label="ページ切り替え">
<button type="button" class="page-nav-btn active" id="pageNav1" onclick="switchPage(1)" aria-current="true">1. プロンプト</button>
<button type="button" class="page-nav-btn" id="pageNav2" onclick="switchPage(2)">2. 翻訳</button>
<button type="button" class="page-nav-btn" id="pageNav3" onclick="switchPage(3)">3. 保存・API</button>
<button type="button" class="page-nav-btn" id="pageNav4" onclick="switchPage(4)">4. 備忘録</button>
<button type="button" class="page-nav-btn" id="pageNav5" onclick="switchPage(5)" style="border-color:rgba(0,229,255,.4);color:var(--accent3);">🚀 5. 生成・投稿</button>
</nav>
<!-- ========== PAGE 2: 翻訳 ========== -->
<div class="page-container" id="page2">
<!-- POSITIVE -->
<div class="card">
<div class="coll-toggle open" onclick="toggleColl(this,'posBody')">
<div class="slabel" style="margin-bottom:0;">▸ ポジティブ プロンプト</div>
<span class="arr"></span>
</div>
<div class="coll-body open" id="posBody">
<textarea id="jpInput" placeholder="例:銀髪のエルフの女の子、森の中で本を読んでいる、柔らかい光、魔法的な雰囲気..."></textarea>
<button class="btn-translate" id="translateBtn" onclick="doTranslate('pos')">✦ 英語に翻訳する</button>
<div class="slabel">▸ 翻訳結果</div>
<div class="output-area empty" id="translatedOutput">← 上で翻訳ボタンを押してください</div>
<div class="row-btns">
<button class="btn btn-c" id="copyTransBtn" onclick="copyBtn(translatedText,'copyTransBtn','COPY')">COPY</button>
</div>
</div>
</div><!-- /posBody -->
<!-- NEGATIVE -->
<div class="card neg-card">
<div class="coll-toggle open" onclick="toggleColl(this,'negTransBody')">
<div class="slabel neg" style="margin-bottom:0;">▸ ネガティブ プロンプト</div>
<span class="arr"></span>
</div>
<div class="coll-body open" id="negTransBody">
<textarea id="jpNegInput" class="neg-ta" placeholder="例:低品質、ぼやけた、変形した手、テキスト、透かし..."></textarea>
<button class="btn-translate neg-btn" id="translateNegBtn" onclick="doTranslate('neg')">✦ ネガティブを翻訳する</button>
<div class="slabel neg">▸ 翻訳結果(ネガティブ)</div>
<div class="output-area neg-color empty" id="translatedNegOutput">← ネガティブを翻訳してください</div>
<div class="row-btns">
<button class="btn btn-c" style="border-color:#ff9f9f;color:#ff9f9f;" id="copyNegBtn" onclick="copyBtn(translatedNegText,'copyNegBtn','COPY')">COPY</button>
</div>
</div>
</div><!-- /negTransBody -->
<!-- HISTORY (2ページ目) -->
<div class="card">
<div class="coll-toggle" onclick="toggleColl(this,'histBody')">
<div class="slabel" style="margin-bottom:0;">▸ 翻訳履歴</div>
<span class="arr"></span>
</div>
<div class="coll-body closed" id="histBody">
<div class="save-list" id="historyList"><div class="empty-saves">まだ履歴がありません</div></div>
<div class="row-btns" style="margin-top:7px;">
<button class="btn btn-d" onclick="clearHistory()">履歴クリア</button>
</div>
</div>
</div>
</div><!-- /page2 -->
<!-- ========== PAGE 1: プロンプト ========== -->
<div class="page-container active" id="page1">
<div class="sub-mode-bar">
<button id="subModeIllust" class="sub-mode-btn active" onclick="switchSubMode('illust')">🎨 イラスト</button>
<button id="subModePhoto" class="sub-mode-btn" onclick="switchSubMode('photo')">📷 実写</button>
</div>
<!-- CHARACTER PRESETS -->
<div class="card" id="charPresetCard" style="border-color:rgba(255,215,0,.2);">
<div class="coll-toggle open" onclick="toggleColl(this,'charPresetBody')">
<div class="slabel" style="margin-bottom:0;color:#ffd740;">📦 キャラクタープリセット
<span id="cpActiveLabel" style="font-family:'Noto Sans JP',sans-serif;font-size:10px;font-weight:normal;color:#aaa;margin-left:4px;letter-spacing:0;text-transform:none;"></span>
</div>
<span class="arr"></span>
</div>
<div class="coll-body open" id="charPresetBody">
<div style="margin-bottom:6px;">
<div style="font-family:'Orbitron',monospace;font-size:8px;color:#888;letter-spacing:1px;margin-bottom:5px;">◾ 内蔵プリセット</div>
<div class="cp-grid" id="cpBuiltinList"></div>
</div>
<div id="cpUserSection" style="margin-bottom:4px;">
<div style="font-family:'Orbitron',monospace;font-size:8px;color:#888;letter-spacing:1px;margin-bottom:5px;">
◾ マイプリセット <span id="cpUserCount" style="color:#7c4dff;font-family:'Orbitron',monospace;"></span>
</div>
<div class="cp-grid" id="cpUserList"><span style="font-size:10px;color:var(--text2);">まだ保存なし</span></div>
</div>
<div class="cp-save-row">
<input id="cpSaveIcon" class="save-name-input" placeholder="🎭" maxlength="2"
style="width:42px;text-align:center;font-size:16px;padding:5px 4px;flex-shrink:0;">
<input id="cpSaveName" class="save-name-input" placeholder="プリセット名(例:俺の嫁)" maxlength="30" style="flex:1;font-size:11px;">
<button class="btn btn-save" onclick="cpSaveCurrent()" style="white-space:nowrap;font-size:10px;flex-shrink:0;">💾 現在の選択を保存</button>
</div>
</div>
</div>
<!-- PRESETS + SEARCH -->
<div class="card">
<div class="coll-toggle open" onclick="toggleColl(this,'presetBody')">
<div class="slabel" style="margin-bottom:0;">▸ モデル別タグ <span class="tag-count" id="tagCount">0 selected</span></div>
<span class="arr"></span>
</div>
<div class="coll-body open" id="presetBody">
<div class="search-wrap">
<input class="search-input" id="searchInput" placeholder="🔍 タグを検索... (例: quality, score, cute)" oninput="onSearch()" autocomplete="off">
<button class="search-clear" onclick="clearSearch()">×</button>
</div>
<!-- 汎用 + 外見 + 検索 -->
<div class="preset-tabs" style="margin-bottom:5px;">
<button class="ptab ptab-general" onclick="switchPreset('general')">汎用</button><button class="wc-btn" data-wc="__pf_general__" onclick="insertWildcard('general','pf_general')" title="汎用タグをwildcardとして挿入">WC</button>
<button class="ptab ptab-general" onclick="switchPreset('angle')">📷 アングル</button><button class="wc-btn" data-wc="__pf_angle__" onclick="insertWildcard('angle','pf_angle')" title="アングル・画角をwildcardとして挿入">WC</button>
<button class="ptab ptab-rating" onclick="switchPreset('rating')">⭐ レーティング</button>
<button class="ptab ptab-year illust-only" onclick="switchPreset('year')">📅 年代</button>
<button class="ptab ptab-chara" onclick="switchPreset('chara')">👤 キャラ</button>
<button class="ptab ptab-chara illust-only" onclick="switchPreset('chara_attr')">🎭 属性</button><button class="wc-btn illust-only" data-wc="__pf_chara_attr__" onclick="insertWildcard('chara_attr','pf_chara_attr')" title="キャラ属性をwildcardとして挿入">WC</button>
<button class="ptab ptab-appear" onclick="switchPreset('appear_haircolor')">✨ 外見 ▾</button>
<button class="ptab ptab-costume" onclick="switchPreset('costume_casual')">👗 衣装 ▾</button>
<button class="ptab ptab-style illust-only" id="ptab-style" onclick="switchPreset('style')">🎨 絵柄</button><button class="wc-btn illust-only" data-wc="__pf_style__" onclick="insertWildcard('style','pf_style')" title="絵柄をwildcardとして挿入">WC</button>
<button class="ptab" id="searchTab" style="display:none;" onclick="switchPreset('_search')">🔍検索</button>
<button class="all-btn" id="allBtn" onclick="toggleSelectAll()">ALL ON</button>
</div>
<!-- キャラクター 作品選択 -->
<div id="charaSeriesRow" style="display:none;margin-bottom:5px;">
<div class="preset-tabs" style="padding:6px 8px;background:rgba(249,168,37,.05);border-radius:8px 8px 0 0;border:1px solid rgba(249,168,37,.2);border-bottom:none;gap:6px;flex-wrap:nowrap;align-items:center;">
<span style="font-family:'Orbitron',monospace;font-size:8px;color:#ffe57f;letter-spacing:1px;white-space:nowrap;flex-shrink:0;">📚 作品 ▸</span>
<select id="charaSeriesSelect" onchange="onCharaSeriesChange()" style="flex:1;min-width:0;background:rgba(30,12,48,.9);color:#ffe57f;border:1px solid rgba(249,168,37,.3);border-radius:5px;padding:3px 6px;font-family:'JetBrains Mono',monospace;font-size:10px;cursor:pointer;">
<option value="">-- 作品を選択 (158作品) --</option>
</select>
<span id="charaSeriesCount" style="font-family:'JetBrains Mono',monospace;font-size:9px;color:rgba(255,229,127,.6);flex-shrink:0;"></span>
<button class="wc-btn" data-wc="__pf_character__" onclick="insertWildcard('character','pf_character')" title="キャラ全体をwildcardとして挿入">WC</button>
</div>
<div style="padding:5px 8px;background:rgba(249,168,37,.03);border:1px solid rgba(249,168,37,.2);border-top:none;border-radius:0 0 8px 8px;display:flex;align-items:center;gap:6px;">
<span style="font-family:'Orbitron',monospace;font-size:8px;color:rgba(255,229,127,.5);white-space:nowrap;">🔍 キャラ検索 ▸</span>
<input class="chara-search-input" id="charaSearchInput" type="text" placeholder="キャラ名を検索..." oninput="onCharaSearch()" autocomplete="off">
<button class="search-clear" onclick="clearCharaSearch()" style="padding:2px 6px;font-size:11px;">×</button>
<span id="charaSearchCount" style="font-family:'JetBrains Mono',monospace;font-size:9px;color:rgba(255,229,127,.5);flex-shrink:0;"></span>
</div>
</div>
<!-- 外見サブタブ -->
<div class="preset-tabs appear-subtabs-row" id="appearSubRow" style="display:none;margin-bottom:5px;padding:5px 8px;background:rgba(171,71,188,.05);border-radius:8px;border:1px solid rgba(171,71,188,.18);">
<span style="font-family:'Orbitron',monospace;font-size:8px;color:#e040fb;letter-spacing:1px;margin-right:4px;white-space:nowrap;">外見 ▸</span>
<button class="ptab ptab-appear" onclick="switchPreset('appear_haircolor')">🎨 髪色</button>
<button class="wc-btn" data-wc="__pf_haircolor__" onclick="insertWildcard('appear_haircolor','pf_haircolor')" title="髪色をwildcardとして挿入">WC</button>
<button class="ptab ptab-appear" onclick="switchPreset('appear_hairstyle')">💇 髪型</button>
<button class="wc-btn" data-wc="__pf_hairstyle__" onclick="insertWildcard('appear_hairstyle','pf_hairstyle')" title="髪型をwildcardとして挿入">WC</button>
<button class="ptab ptab-appear" onclick="switchPreset('appear_eye')">👁 目</button>
<button class="wc-btn" data-wc="__pf_appear_eye__" onclick="insertWildcard('appear_eye','pf_appear_eye')" title="目タグをwildcardとして挿入">WC</button>
<button class="ptab ptab-appear" onclick="switchPreset('appear_face')">😊 顔</button>
<button class="wc-btn" data-wc="__pf_appear_face__" onclick="insertWildcard('appear_face','pf_appear_face')" title="顔タグをwildcardとして挿入">WC</button>
<button class="ptab ptab-appear" onclick="switchPreset('appear_skin')">🎨 肌色</button>
<button class="wc-btn" data-wc="__pf_appear_skin__" onclick="insertWildcard('appear_skin','pf_appear_skin')" title="肌色タグをwildcardとして挿入">WC</button>
<button class="ptab ptab-appear" onclick="switchPreset('appear_bodyshape')">🫀 体形</button>
<button class="wc-btn" data-wc="__pf_appear_bodyshape__" onclick="insertWildcard('appear_bodyshape','pf_appear_bodyshape')" title="体形タグをwildcardとして挿入">WC</button>
<button class="ptab ptab-appear" onclick="switchPreset('appear_accessory')">💎 アクセサリー</button>
<button class="wc-btn" data-wc="__pf_appear_accessory__" onclick="insertWildcard('appear_accessory','pf_appear_accessory')" title="アクセサリータグをwildcardとして挿入">WC</button>
<button class="ptab ptab-appear" onclick="switchPreset('appear_height')">📏 身長</button>
<button class="wc-btn" data-wc="__pf_appear_height__" onclick="insertWildcard('appear_height','pf_appear_height')" title="身長タグをwildcardとして挿入">WC</button>
<button class="ptab ptab-appear illust-only" onclick="switchPreset('appear_special')">✨ 特殊</button>
<button class="wc-btn illust-only" data-wc="__pf_appear_special__" onclick="insertWildcard('appear_special','pf_appear_special')" title="特殊外見をwildcardとして挿入">WC</button>
</div>
<!-- 衣装サブタブ -->
<div class="preset-tabs costume-subtabs-row" id="costumeSubRow" style="display:none;margin-bottom:5px;padding:5px 8px;background:rgba(255,183,77,.05);border-radius:8px;border:1px solid rgba(255,183,77,.2);">
<span style="font-family:'Orbitron',monospace;font-size:8px;color:#ffb74d;letter-spacing:1px;margin-right:4px;white-space:nowrap;">衣装 ▸</span>
<button class="ptab ptab-costume" onclick="switchPreset('costume_casual')">👕 日常・カジュアル</button>
<button class="wc-btn" data-wc="__pf_costume_casual__" onclick="insertWildcard('costume_casual','pf_costume_casual')" title="日常・カジュアル衣装をwildcardとして挿入">WC</button>
<button class="ptab ptab-costume" onclick="switchPreset('costume_uniform')">👔 制服・礼装</button>
<button class="wc-btn" data-wc="__pf_costume_uniform__" onclick="insertWildcard('costume_uniform','pf_costume_uniform')" title="制服・礼装をwildcardとして挿入">WC</button>
<button class="ptab ptab-costume illust-only" onclick="switchPreset('costume_fantasy')">⚔️ ファンタジー・特殊</button>
<button class="wc-btn illust-only" data-wc="__pf_costume_fantasy__" onclick="insertWildcard('costume_fantasy','pf_costume_fantasy')" title="ファンタジー・特殊衣装をwildcardとして挿入">WC</button>
<button class="wc-btn illust-only" style="margin-left:8px;background:rgba(255,183,77,.2);border-color:rgba(255,183,77,.6);color:#ffb74d;font-weight:bold;" data-wc="__pf_costume_all__" onclick="insertWildcard('costume_casual','pf_costume_all')" title="全衣装(カジュアル+制服+ファンタジー 計292タグをwildcardとして挿入">✨ 全衣装 WC</button>
</div>
<!-- Animagine ポジティブ / ネガティブ -->
<div class="preset-tabs illust-only" style="margin-bottom:5px;padding:5px 8px;background:rgba(124,77,255,.05);border-radius:8px;border:1px solid rgba(124,77,255,.18);">
<span style="font-family:'Orbitron',monospace;font-size:8px;color:#a78bfa;letter-spacing:1px;margin-right:6px;white-space:nowrap;">ANI ▸</span>
<button class="ptab ptab-animagine" onclick="switchPreset('animagine')">ポジティブ</button>
<button class="ptab ptab-neg-ani" onclick="switchPreset('neg_animagine')">ネガティブ</button>
<button class="ptab ptab-skin-ctrl" onclick="switchPreset('neg_ani_skin')">肌色制御</button>
<button class="all-btn" id="allBtnAni" onclick="toggleSelectAllPreset('animagine','neg_animagine','allBtnAni')" style="margin-left:auto;">ALL ON</button>
</div>
<!-- Pony ポジティブ / ネガティブ -->
<div class="preset-tabs illust-only" style="margin-bottom:5px;padding:5px 8px;background:rgba(255,77,166,.05);border-radius:8px;border:1px solid rgba(255,77,166,.18);">
<span style="font-family:'Orbitron',monospace;font-size:8px;color:#ff80c0;letter-spacing:1px;margin-right:6px;white-space:nowrap;">PONY ▸</span>
<button class="ptab ptab-pony" onclick="switchPreset('pony')">ポジティブ</button>
<button class="ptab ptab-neg-pony" onclick="switchPreset('neg_pony')">ネガティブ</button>
<button class="all-btn" id="allBtnPony" onclick="toggleSelectAllPreset('pony','neg_pony','allBtnPony')" style="margin-left:auto;">ALL ON</button>
</div>
<!-- NSFW sub-tabs row -->
<div class="preset-tabs nsfw-tabs-row illust-only" style="margin-bottom:10px;padding:6px 8px;background:rgba(255,107,107,.05);border-radius:8px;border:1px solid rgba(255,107,107,.15);">
<span style="font-family:'Orbitron',monospace;font-size:8px;color:#ff6b6b;letter-spacing:1px;margin-right:4px;white-space:nowrap;">NSFW ▸</span>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_rating')">レーティング</button><button class="wc-btn" data-wc="__pf_nsfw_rating__" onclick="insertWildcard('nsfw_rating','pf_nsfw_rating')" title="レーティングをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_body_f')">女性の体</button><button class="wc-btn" data-wc="__pf_nsfw_body_f__" onclick="insertWildcard('nsfw_body_f','pf_nsfw_body_f')" title="女性の体をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_body_m')">男性の体</button><button class="wc-btn" data-wc="__pf_nsfw_body_m__" onclick="insertWildcard('nsfw_body_m','pf_nsfw_body_m')" title="男性の体をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_position')">体位</button><button class="wc-btn" data-wc="__pf_nsfw_position__" onclick="insertWildcard('nsfw_position','pf_nsfw_position')" title="体位をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_vaginal')">膣内</button><button class="wc-btn" data-wc="__pf_nsfw_vaginal__" onclick="insertWildcard('nsfw_vaginal','pf_nsfw_vaginal')" title="膣内をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_oral')">口淫</button><button class="wc-btn" data-wc="__pf_nsfw_oral__" onclick="insertWildcard('nsfw_oral','pf_nsfw_oral')" title="口淫をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_anal')">アナル</button><button class="wc-btn" data-wc="__pf_nsfw_anal__" onclick="insertWildcard('nsfw_anal','pf_nsfw_anal')" title="アナルをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_handjob')">手コキ</button><button class="wc-btn" data-wc="__pf_nsfw_handjob__" onclick="insertWildcard('nsfw_handjob','pf_nsfw_handjob')" title="手コキをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_paizuri')">パイズリ</button><button class="wc-btn" data-wc="__pf_nsfw_paizuri__" onclick="insertWildcard('nsfw_paizuri','pf_nsfw_paizuri')" title="パイズリをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_cum')">射精</button><button class="wc-btn" data-wc="__pf_nsfw_cum__" onclick="insertWildcard('nsfw_cum','pf_nsfw_cum')" title="射精をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_squirt')">潮吹き</button><button class="wc-btn" data-wc="__pf_nsfw_squirt__" onclick="insertWildcard('nsfw_squirt','pf_nsfw_squirt')" title="潮吹きをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_reaction')">反応・顔</button><button class="wc-btn" data-wc="__pf_nsfw_reaction__" onclick="insertWildcard('nsfw_reaction','pf_nsfw_reaction')" title="反応・顔をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_costume')">衣装</button><button class="wc-btn" data-wc="__pf_nsfw_costume__" onclick="insertWildcard('nsfw_costume','pf_nsfw_costume')" title="衣装をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_bondage')">拘束</button><button class="wc-btn" data-wc="__pf_nsfw_bondage__" onclick="insertWildcard('nsfw_bondage','pf_nsfw_bondage')" title="拘束をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_yuri')">百合</button><button class="wc-btn" data-wc="__pf_nsfw_yuri__" onclick="insertWildcard('nsfw_yuri','pf_nsfw_yuri')" title="百合をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_futa')">フタナリ</button><button class="wc-btn" data-wc="__pf_nsfw_futa__" onclick="insertWildcard('nsfw_futa','pf_nsfw_futa')" title="フタナリをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_monster')">触手/怪物</button><button class="wc-btn" data-wc="__pf_nsfw_monster__" onclick="insertWildcard('nsfw_monster','pf_nsfw_monster')" title="触手/怪物をwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_scenario')">シナリオ</button><button class="wc-btn" data-wc="__pf_nsfw_scenario__" onclick="insertWildcard('nsfw_scenario','pf_nsfw_scenario')" title="シナリオをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_fetish')">フェチ</button><button class="wc-btn" data-wc="__pf_nsfw_fetish__" onclick="insertWildcard('nsfw_fetish','pf_nsfw_fetish')" title="フェチをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_pose')">エロポーズ</button><button class="wc-btn" data-wc="__pf_nsfw_pose__" onclick="insertWildcard('nsfw_pose','pf_nsfw_pose')" title="エロポーズをwildcardとして挿入">WC</button>
<button class="ptab ptab-nsfw" onclick="switchPreset('nsfw_misc')">その他</button><button class="wc-btn" data-wc="__pf_nsfw_misc__" onclick="insertWildcard('nsfw_misc','pf_nsfw_misc')" title="その他をwildcardとして挿入">WC</button>
<button class="btn btn-rand" id="nsfwAllBtn" style="margin-left:auto;white-space:nowrap;font-size:8px;padding:4px 10px;" onclick="randomAllNsfw()">🎲 全タブ一括</button>
</div>
<!-- ===== 実写専用プリセット行 ===== -->
<div class="preset-tabs photo-only" style="margin-bottom:5px;padding:5px 8px;background:rgba(0,229,255,.05);border-radius:8px;border:1px solid rgba(0,229,255,.18);">
<span style="font-family:'Orbitron',monospace;font-size:8px;color:var(--accent3);letter-spacing:1px;margin-right:6px;white-space:nowrap;">📷 写真 ▸</span>
<button class="ptab" style="border-color:rgba(0,229,255,.4);color:var(--accent3);" onclick="switchPreset('photo_quality')">📷 写真品質</button>
<button class="ptab" style="border-color:rgba(0,229,255,.4);color:var(--accent3);" onclick="switchPreset('photo_subject')">👤 被写体</button>
<button class="ptab" style="border-color:rgba(0,229,255,.4);color:var(--accent3);" onclick="switchPreset('photo_location')">🏙 ロケーション</button>
<button class="ptab" style="border-color:rgba(0,229,255,.4);color:var(--accent3);" onclick="switchPreset('photo_uniform')">🎓 制服・学校</button>
<button class="ptab" style="border-color:rgba(0,229,255,.4);color:var(--accent3);" onclick="switchPreset('photo_lighting')">💡 照明</button>
<button class="ptab" style="border-color:rgba(255,107,107,.4);color:#ff9f9f;" onclick="switchPreset('photo_neg')">📉 写真ネガ</button>
</div>
<div class="tags-grid" id="tagsGrid"></div>
<div class="divider"></div>
<div class="slabel" style="color:var(--accent2);">▸ 選択中タグ(ドラッグで並び替え)</div>
<div class="selected-tags-zone" id="selZone"><span class="empty-zone">タグをクリックして追加</span></div>
<div class="neg-sel-label">▸ ネガティブ選択タグ</div>
<div class="neg-sel-zone" id="negSelZone"><span class="empty-zone">ネガティブタグをクリックして追加</span></div>
<div class="row-btns">
<button class="btn" onclick="appendToFinal()">↓ 最終出力に反映</button>
<button class="btn btn-c" id="copyTagsBtn" onclick="copyBtn(getSortedSelTags().map(s=>s.tag).join(', '),'copyTagsBtn','COPY TAGS')">COPY TAGS</button>
<button class="btn btn-rand" id="randOneBtn" onclick="randomOne()" style="display:none;">🎲 1つランダム</button>
<button class="btn btn-d" onclick="clearTags()">CLEAR</button>
</div>
<div id="randomResult" class="random-result" style="display:none;"></div>
</div>
</div><!-- /presetBody -->
<!-- WILDCARD SHELF -->
<div class="card wc-shelf-card" style="border-color:rgba(255,193,7,.25);background:var(--surface);">
<div class="card" style="position:absolute;top:0;left:0;right:0;height:2px;background:linear-gradient(90deg,#f39c12,#ffd54f);opacity:.7;border-radius:12px 12px 0 0;"></div>
<div class="coll-toggle open" onclick="toggleColl(this,'wcShelfBody')">
<div class="slabel" style="margin-bottom:0;color:#ffd54f;">▸ Wildcard 置き場 <span style="font-size:8px;opacity:.6;font-family:'Noto Sans JP',sans-serif;letter-spacing:0;margin-left:4px;">TXTファイルと対応</span></div>
<div style="display:flex;gap:5px;margin-left:auto;flex-shrink:0;" onclick="event.stopPropagation()">
<button class="wc-all-btn" id="wcAllOnBtn" onclick="wcShelfAllOn()">ALL ON</button>
<button class="wc-all-btn wc-all-off" id="wcAllOffBtn" onclick="wcShelfAllOff()">ALL OFF</button>
</div>
<span class="arr" style="color:#ffd54f;margin-left:6px;"></span>
</div>
<div class="coll-body open" id="wcShelfBody">
<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:2px;">
<button class="wc-shelf-btn" data-wc="__pf_hair__" onclick="insertWildcard('hair','pf_hair')">
<span class="wcsb-icon">💇</span><span class="wcsb-text"><span class="wcsb-label">Hair</span><span class="wcsb-tag">__pf_hair__</span></span>
</button>
<button class="wc-shelf-btn" data-wc="__pf_fetish__" onclick="insertWildcard('fetish','pf_fetish')">
<span class="wcsb-icon"></span><span class="wcsb-text"><span class="wcsb-label">Fetish</span><span class="wcsb-tag">__pf_fetish__</span></span>
</button>
<button class="wc-shelf-btn" data-wc="__pf_lewd__" onclick="insertWildcard('lewd','pf_lewd')">
<span class="wcsb-icon">🔥</span><span class="wcsb-text"><span class="wcsb-label">Lewd</span><span class="wcsb-tag">__pf_lewd__</span></span>
</button>
<button class="wc-shelf-btn" data-wc="__pf_location__" onclick="insertWildcard('location','pf_location')">
<span class="wcsb-icon">🏩</span><span class="wcsb-text"><span class="wcsb-label">Location</span><span class="wcsb-tag">__pf_location__</span></span>
</button>
<button class="wc-shelf-btn" data-wc="__pf_maniac_clothes__" onclick="insertWildcard('maniac_clothes','pf_maniac_clothes')">
<span class="wcsb-icon">👗</span><span class="wcsb-text"><span class="wcsb-label">Maniac Clothes</span><span class="wcsb-tag">__pf_maniac_clothes__</span></span>
</button>
<button class="wc-shelf-btn" data-wc="__pf_artist__" onclick="insertWildcard('artist','pf_artist')">
<span class="wcsb-icon">🎨</span><span class="wcsb-text"><span class="wcsb-label">Artist</span><span class="wcsb-tag">__pf_artist__</span></span>
</button>
<button class="wc-shelf-btn" data-wc="__pf_pose__" onclick="insertWildcard('pose','pf_pose')">
<span class="wcsb-icon">🤸</span><span class="wcsb-text"><span class="wcsb-label">Pose</span><span class="wcsb-tag">__pf_pose__</span></span>
</button>
</div>
<div style="margin-top:10px;padding:8px 10px;background:rgba(255,193,7,.06);border:1px solid rgba(255,193,7,.2);border-radius:8px;">
<div style="font-family:'Orbitron',monospace;font-size:8px;letter-spacing:1px;color:#ffd54f;margin-bottom:6px;">▸ Wildcard TXT 出力(現在のタグ一覧から自動作成)</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:6px;">
<label style="font-size:9px;color:rgba(255,255,255,.6);white-space:nowrap;">保存先フォルダ:</label>
<input type="text" id="wcExportFolder" class="save-name-input" placeholder="wildcards" value="wildcards" maxlength="120" style="flex:1;min-width:100px;font-size:10px;padding:5px 8px;">
</div>
<div style="display:flex;flex-wrap:wrap;gap:6px;">
<button type="button" class="btn btn-save" style="font-size:9px;white-space:nowrap;" onclick="exportWildcardsAsZip()" title="現在のPRESETS内容で全TXTをZIPにまとめてダウンロード">📦 TXT を ZIP でDL</button>
<button type="button" class="btn" style="border-color:#00e5a0;color:#00e5a0;background:rgba(0,229,160,.08);font-size:9px;white-space:nowrap;" onclick="exportWildcardsToFolder()" title="フォルダを選んでTXTを直接保存Chrome等">📁 フォルダに保存</button>
</div>
<div style="margin-top:6px;font-size:8px;color:rgba(255,255,255,.4);font-family:'Noto Sans JP',sans-serif;line-height:1.5;">※ 内容は常に現在のタグ一覧から生成されます。HTML内のPRESETSを編集したら、再度エクスポートすると反映されます。</div>
</div>
<div style="margin-top:8px;font-size:9px;color:rgba(255,255,255,.25);font-family:'JetBrains Mono',monospace;line-height:1.8;">
▸ TXTは Fooocus の <code style="background:rgba(255,255,255,.06);padding:1px 5px;border-radius:3px;">wildcards/</code> フォルダに配置
</div>
</div>
</div>
</div><!-- /page1 -->
<!-- ========== PAGE 3: 保存・API ========== -->
<div class="page-container" id="page3">
<!-- プロンプト保存・呼び出し -->
<div class="card save-card">
<div class="coll-toggle" onclick="toggleColl(this,'saveBody')">
<div class="slabel save-col" style="margin-bottom:0;">▸ プロンプト保存・呼び出し</div>
<span class="arr"></span>
</div>
<div class="coll-body open" id="saveBody">
<div class="save-name-row">
<input class="save-name-input" id="saveNameInput" placeholder="保存名エルフ女の子_森" maxlength="40">
<label class="btn btn-save" style="white-space:nowrap;cursor:pointer;" title="参考画像を添付(任意)">
🖼️
<input type="file" id="saveImageInput" accept="image/*" style="display:none;" onchange="onImageSelect(event)">
</label>
<button class="btn btn-save" onclick="savePrompt()" style="white-space:nowrap;">💾 新規保存</button>
<button class="btn btn-save" onclick="overwriteSave()" style="white-space:nowrap;" title="同名の保存データを上書き">♻ 上書き</button>
</div>
<div class="row-btns" style="margin-bottom:8px;">
<button class="btn" onclick="downloadSaves()" style="border-color:#00e5a0;color:#00e5a0;background:rgba(0,229,160,.08);">⬇ DL</button>
<label class="btn" style="cursor:pointer;border-color:#00e5ff;color:#00e5ff;background:rgba(0,229,255,.08);" title="JSONファイルをインポート">
⬆ インポート
<input type="file" id="importSavesInput" accept=".json" style="display:none;" onchange="importSaves(event)">
</label>
</div>
<div id="saveImagePreview" style="display:none;margin-bottom:8px;">
<img id="saveImgThumb" style="height:48px;border-radius:6px;border:1px solid var(--border);vertical-align:middle;">
<button class="btn btn-d" style="font-size:8px;margin-left:6px;" onclick="clearImageInput()"></button>
</div>
<div class="save-list" id="saveList"><div class="empty-saves">保存なし</div></div>
<input type="file" id="addImgInput" accept="image/*" style="display:none;" onchange="onAddImgSelect(event)">
</div>
</div>
<!-- API KEY (3ページ目) -->
<div class="card key-card">
<div class="slabel key-col">▸ Anthropic API Key</div>
<div style="display:flex;gap:8px;align-items:center;">
<input type="password" class="apikey-input" id="apikeyInput" placeholder="sk-ant-api03-..." oninput="onKeyInput()">
<span class="apikey-status no" id="keyStatus">未設定</span>
</div>
<div style="font-size:10px;color:var(--text2);margin-top:6px;">
※ キーは <a href="https://console.anthropic.com/settings/keys" target="_blank" style="color:var(--accent3);">console.anthropic.com</a> で取得。このページ内にのみ保持されます。
</div>
</div>
</div><!-- /page3 -->
<!-- ========== PAGE 4: プロンプト備忘録(独立) ========== -->
<div class="page-container" id="page4">
<div class="card save-card">
<div class="coll-toggle open" onclick="toggleColl(this,'memoBody')">
<div class="slabel save-col" style="margin-bottom:0;">▸ プロンプト備忘録(独立保存)</div>
<span class="arr"></span>
</div>
<div class="coll-body open" id="memoBody">
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:8px;">
<input class="save-name-input" id="memoTitle" placeholder="タイトル(例:森のエルフ)" maxlength="60" style="flex:1;min-width:180px;">
<select id="memoModel" class="save-name-input" style="flex:0 0 auto;min-width:190px;padding:7px 10px;">
<option value="Fooocus/Animagine">Fooocus / Animagine</option>
<option value="nanobanana2">nanobanana2</option>
<option value="Other">Other</option>
</select>
</div>
<textarea id="memoPrompt" placeholder="このページ専用のプロンプトを入力(他ページとは無関係)" style="min-height:120px;"></textarea>
<div style="display:flex;flex-wrap:wrap;gap:4px;align-items:center;margin-top:6px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.1);border-radius:6px;padding:5px 8px;min-height:32px;" id="memoTagInputWrap">
<div id="memoTagChipsForm" style="display:contents;"></div>
<input id="memoTagInput" placeholder="タグ追加Enter/," style="flex:1;min-width:80px;background:none;border:none;outline:none;color:inherit;font-size:10px;padding:0;" onkeydown="memoTagKeydown(event)">
</div>
<div id="memoTagPresets" style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px;"></div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:8px;">
<label class="btn btn-save" style="cursor:pointer;white-space:nowrap;" title="画像を追加前でも後でもOK">
🖼️ 画像追加
<input type="file" id="memoImageInput" accept="image/*" multiple style="display:none;" onchange="memoAddImages(event)">
</label>
<button type="button" class="btn btn-save" onclick="memoSaveNew()" style="white-space:nowrap;">💾 新規保存</button>
<button type="button" class="btn btn-save" onclick="memoUpdateSelected()" style="white-space:nowrap;" title="選択中の備忘録を更新">♻ 更新</button>
<button type="button" class="btn btn-d" onclick="memoDeleteSelected()" style="white-space:nowrap;">🗑 削除</button>
<span style="margin-left:auto;font-size:9px;color:rgba(255,255,255,.45);font-family:'JetBrains Mono',monospace;" id="memoStatus">未保存</span>
</div>
<div style="margin-top:10px;padding:8px 10px;border:1px solid rgba(0,229,160,.25);background:rgba(0,229,160,.05);border-radius:8px;">
<div style="display:flex;flex-wrap:wrap;gap:8px;align-items:center;">
<button type="button" class="btn btn-rand" onclick="memoChooseSyncFolder()" style="white-space:nowrap;">📁 同期フォルダ選択</button>
<span id="memoSyncFolderName" style="font-size:9px;color:rgba(0,229,160,.85);max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title=""></span>
<label style="display:flex;align-items:center;gap:6px;font-size:10px;color:rgba(255,255,255,.65);cursor:pointer;user-select:none;">
<input type="checkbox" id="memoAutoSync" onchange="memoSetAutoSync()" style="accent-color: var(--success);">
自動でフォルダに同期
</label>
<button type="button" class="btn" onclick="memoSyncNow()" style="border-color:#00e5ff;color:#00e5ff;background:rgba(0,229,255,.08);white-space:nowrap;">⟳ 今すぐ同期</button>
<button type="button" class="btn" onclick="memoExportJson()" style="white-space:nowrap;">⬇ JSONでDL</button>
<label class="btn" style="cursor:pointer;white-space:nowrap;" title="JSONをインポート画像は端末内DBにある分のみ表示されます">
⬆ JSONインポート
<input type="file" id="memoImportInput" accept=".json" style="display:none;" onchange="memoImportJson(event)">
</label>
</div>
<div style="margin-top:6px;font-size:9px;color:rgba(255,255,255,.4);line-height:1.5;">
※ 自動同期は対応ブラウザChrome/Edge等で、最初にフォルダ許可が必要です。非対応でもページ内保存JSONバックアップは使えます。
</div>
</div>
<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap;">
<div style="flex:1;min-width:240px;">
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
<div class="slabel save-col" style="margin-bottom:0;flex:1;">▸ 保存一覧(検索)</div>
<select id="memoSort" onchange="memoRenderList()" style="background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.15);color:rgba(255,255,255,.75);font-size:9px;border-radius:5px;padding:3px 6px;cursor:pointer;">
<option value="updated_desc">更新↓</option>
<option value="updated_asc">更新↑</option>
<option value="created_desc">作成↓</option>
<option value="title_asc">タイトルA-Z</option>
<option value="title_desc">タイトルZ-A</option>
</select>
</div>
<div id="memoTagFilterRow" style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:6px;"></div>
<div class="search-wrap">
<input class="search-input" id="memoSearch" placeholder="🔍 タイトル/プロンプト/モデルを検索..." oninput="memoRenderList()" autocomplete="off">
<button class="search-clear" onclick="document.getElementById('memoSearch').value='';memoRenderList()">×</button>
</div>
<div class="save-list" id="memoList"><div class="empty-saves">保存なし</div></div>
</div>
<div style="flex:1;min-width:240px;">
<div class="slabel save-col" style="margin-bottom:6px;">▸ 選択中(コピー・画像・メタデータ)</div>
<div class="row-btns" style="margin-bottom:8px;">
<button class="btn btn-c" id="memoCopyBtn" onclick="memoCopyPrompt()" style="white-space:nowrap;">COPY PROMPT</button>
<button class="btn" onclick="memoCopySummary()" style="white-space:nowrap;border-color:#00e5ff;color:#00e5ff;background:rgba(0,229,255,.08);">COPY SUMMARY</button>
</div>
<div class="output-area empty" id="memoPreview">左の一覧から選択してください</div>
<div style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;" id="memoImages"></div>
<div style="margin-top:8px;" class="output-area empty" id="memoMeta">画像メタデータはここに表示</div>
</div>
</div>
</div>
</div>
</div><!-- /page4 -->
<!-- ========== PAGE 5: 画像生成 ========== -->
<div class="page-container" id="page5">
<!-- Fooocus API コネクション設定カード -->
<div class="card gen-card">
<div class="slabel key-col">▸ Fooocus API 接続設定</div>
<div style="display:flex;gap:8px;align-items:center;margin-bottom:6px;">
<input type="text" class="apikey-input" id="fooocusEndpointInput"
placeholder="http://127.0.0.1:8080"
oninput="onFooocusEndpointInput()"
style="flex:1;">
<button class="btn btn-c" id="fooocusTestBtn" onclick="testFooocusConnection()" style="white-space:nowrap;">⚡ 接続テスト</button>
<span class="fooocus-status offline" id="fooocusStatus">
<span class="status-dot offline" id="fooocusDot"></span>
<span id="fooocusStatusText">オフライン</span>
</span>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-bottom:4px;">
<span style="font-size:9px;color:var(--text2);font-family:'JetBrains Mono',monospace;">API Path:</span>
<select class="gen-select" id="fooocusApiPath" onchange="onFooocusEndpointInput()" style="font-size:10px;padding:4px 8px;">
<option value="/v1/generation/text-to-image">REST /v1/generation/text-to-image (推奨)</option>
<option value="/run/predict">Gradio /run/predict</option>
<option value="/api/predict">Gradio /api/predict</option>
</select>
</div>
<div style="font-size:10px;color:var(--text2);margin-top:4px;line-height:1.6;">
<strong style="color:var(--success);">run_with_forge.bat</strong> でFooocusを起動すると自動的に接続できます。デフォルト: <strong style="color:var(--accent3);">http://127.0.0.1:8080</strong>
</div>
</div>
<!-- 生成コントロールパネル -->
<div class="card gen-card" style="border-color:rgba(124,77,255,.3);">
<div class="slabel" style="color:var(--accent);">▸ 生成パラメータ</div>
<!-- アスペクト比・枚数 -->
<div class="gen-param-row">
<span class="gen-param-label">📐 サイズ</span>
<select class="gen-select" id="genAspect">
<option value="1152x896">1152×896 (横長 16:9)</option>
<option value="1024x1024" selected>1024×1024 (正方形 1:1)</option>
<option value="896x1152">896×1152 (縦長 3:4)</option>
<option value="832x1216">832×1216 (縦長 2:3)</option>
<option value="1216x832">1216×832 (横長 3:2)</option>
<option value="1344x768">1344×768 (横長 ワイド)</option>
<option value="768x1344">768×1344 (縦長 ポスター)</option>
</select>
<span class="gen-param-label" style="margin-left:8px;">🖼 枚数</span>
<select class="gen-select" id="genImageNum">
<option value="1" selected>1枚</option>
<option value="2">2枚</option>
<option value="4">4枚</option>
</select>
<span class="gen-param-label" style="margin-left:8px;">🎲 シード</span>
<input type="number" class="gen-select" id="genSeed" placeholder="-1 (ランダム)" value="-1"
style="width:110px;padding:6px 8px;">
<button class="btn btn-rand" style="font-size:9px;padding:4px 8px;" onclick="randomizeSeed()" title="シードをランダム化">🎲</button>
</div>
<!-- 品質・ステップFooocusのデフォルトが優秀なので折りたたみ -->
<details style="margin-bottom:10px;">
<summary style="cursor:pointer;font-family:'Orbitron',monospace;font-size:8px;letter-spacing:1px;color:var(--text2);user-select:none;padding:4px 0;">▸ 詳細設定(クリックで展開)</summary>
<div style="padding-top:8px;">
<div class="gen-param-row">
<span class="gen-param-label">⚡ ステップ数</span>
<input type="range" id="genSteps" min="10" max="60" value="30" step="5"
oninput="document.getElementById('genStepsVal').textContent=this.value"
style="flex:1;accent-color:var(--accent);">
<span id="genStepsVal" style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--accent3);width:24px;text-align:right;">30</span>
</div>
<div class="gen-param-row">
<span class="gen-param-label">🎨 CFG Scale</span>
<input type="range" id="genCfg" min="1" max="15" value="7" step=".5"
oninput="document.getElementById('genCfgVal').textContent=this.value"
style="flex:1;accent-color:var(--accent2);">
<span id="genCfgVal" style="font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--accent3);width:28px;text-align:right;">7</span>
</div>
<div class="gen-param-row">
<span class="gen-param-label">🔧 Refiner</span>
<label style="display:flex;align-items:center;gap:6px;font-size:10px;color:var(--text2);cursor:pointer;">
<input type="checkbox" id="genRefiner" checked style="accent-color:var(--accent);">
有効(推奨)
</label>
<span class="gen-param-label" style="margin-left:12px;">📜 スタイル</span>
<select class="gen-select" id="genStyle" style="font-size:10px;padding:4px 8px;">
<option value="Fooocus V2">Fooocus V2 (デフォルト)</option>
<option value="Fooocus Enhance">Fooocus Enhance</option>
<option value="Fooocus Sharp">Fooocus Sharp</option>
<option value="Fooocus Semi Realistic">Semi Realistic</option>
<option value="SAI Anime">SAI Anime</option>
<option value="SAI Digital Art">SAI Digital Art</option>
<option value="SAI Fantasy Art">SAI Fantasy Art</option>
<option value="MRE Cinematic Dynamic">MRE Cinematic</option>
<option value="Artstyle Impressionist">Impressionist</option>
</select>
</div>
</div>
</details>
<!-- プロンプト確認エリア(編集可能) -->
<div style="margin-bottom:10px;">
<div class="label-s" style="margin-bottom:4px;">▸ POSITIVE最終プロンプトから自動取得・編集可</div>
<textarea id="genPosInput" placeholder="プロンプトを入力するか、最終プロンプトを確認してください"
style="min-height:60px;font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--success);"
oninput="syncGenPromptManual()"></textarea>
<div class="label-s" style="margin-bottom:4px;margin-top:6px;color:var(--neg);">▸ NEGATIVE最終プロンプトから自動取得・編集可</div>
<textarea id="genNegInput" placeholder="ネガティブプロンプト(任意)"
class="neg-ta" style="min-height:40px;font-family:'JetBrains Mono',monospace;font-size:11px;color:#ff9f9f;"
oninput="syncGenPromptManual()"></textarea>
<div class="row-btns" style="margin-top:6px;">
<button class="btn" onclick="syncGenFromFinal()" style="font-size:9px;">⟳ 最終プロンプトから再取得</button>
</div>
</div>
<!-- メイン生成ボタン -->
<button class="btn-generate" id="genMainBtn" onclick="generateImage()">
🚀 GENERATE IMAGE
</button>
<!-- ローディング表示 -->
<div class="gen-loading" id="genLoading">
<div class="gen-loading-text">⚡ GENERATING...</div>
<div class="gen-progress-bar"><div class="gen-progress-fill"></div></div>
<div class="gen-elapsed" id="genElapsed">経過時間: 0s</div>
</div>
<!-- エラー表示 -->
<div class="gen-error" id="genError">
<div class="gen-error-title">⚠ エラーが発生しました</div>
<div id="genErrorMsg"></div>
</div>
</div>
<!-- 生成結果ギャラリー -->
<div class="card gallery-card" id="genGalleryCard">
<div class="coll-toggle open" onclick="toggleColl(this,'genGalleryBody')">
<div class="slabel" style="margin-bottom:0;color:var(--accent2);">▸ 生成済み画像ギャラリー <span id="genGalleryCount" style="font-size:8px;color:var(--text2);margin-left:4px;font-family:'JetBrains Mono',monospace;"></span></div>
<div style="margin-left:auto;display:flex;gap:6px;" onclick="event.stopPropagation()">
<button class="btn btn-d" style="font-size:8px;" onclick="clearGallery()">🗑 クリア</button>
</div>
<span class="arr" style="margin-left:6px;"></span>
</div>
<div class="coll-body open" id="genGalleryBody">
<div class="gen-gallery" id="genGallery">
<div class="gen-gallery-empty">生成した画像がここに表示されます</div>
</div>
</div>
</div>
</div><!-- /page5 -->
<!-- 画像拡大モーダル -->
<div class="gen-img-modal" id="genImgModal" onclick="closeImgModal()">
<span class="gen-img-modal-close" onclick="closeImgModal()"></span>
<img id="genImgModalSrc" src="" alt="Generated image">
</div>
<p class="api-note">※ APIキーはこのページ内のみで使用。外部送信なしAnthropic APIへの翻訳リクエストを除く</p>
<script src="https://cdnjs.cloudflare.com/ajax/libs/exifr/7.1.3/exifr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script>
/* ===== PRESETS ===== */
const PRESETS = {
// ── 汎用 ──
general:[
'pure white background','simple background','white background',
'1girl','2girls','1boy','1other','harem',
'looking at viewer','full body'
],
// ── アングル・画角・フレーミング ──
angle:[
// カメラ方向
'from above','from below','from behind','from side','from front',
'bird\'s-eye view','worm\'s-eye view','aerial view','overhead view','top-down view',
'low angle','high angle','eye level','dutch angle','tilted angle',
'three-quarter view','isometric view','diagonal angle',
// ショットサイズ
'extreme close-up','close-up','medium close-up','medium shot',
'long shot','wide shot','full shot',
// フレーミング(体の写る範囲)
'full body','upper body','lower body','half body',
'bust shot','waist up','cowboy shot','head shot','face focus',
'chest up','knees up','thighs up','portrait',
// POV・主観
'pov','first person view','point of view','selfie angle',
'over the shoulder shot','vanishing point',
// 視線・向き
'looking at viewer','looking back','looking up','looking down','looking away',
'looking to the side','looking over shoulder','eye contact','direct gaze',
'turning around','facing away','facing viewer','back to viewer',
// レンズ・光学効果
'fisheye lens','wide angle lens','telephoto lens','macro shot',
'depth of field','bokeh','shallow focus','motion blur','lens flare',
// 構図・配置
'centered','off-center','symmetrical','negative space',
'dramatic angle','dynamic angle','action angle',
'profile view','rear view','side view','front view',
'solo focus','character focus'
],
// ── レーティング ──
rating:[
'safe','sensitive','nsfw','explicit'
],
// ── 年代タグ ──
year:[
'year 2005','year 2006','year 2007','year 2008','year 2009','year 2010',
'year 2011','year 2012','year 2013','year 2014','year 2015','year 2016',
'year 2017','year 2018','year 2019','year 2020','year 2021','year 2022',
'year 2023','year 2024','year 2025'
],
// ── キャラ構図 ──
chara:[
'1girl','1boy','solo','solo focus','multiple girls','multiple boys','2girls','2boys',
'girl and boy','couple','group',
'looking at viewer','looking away','looking back','looking up','looking down',
'from above','from below','from side','from behind','dutch angle','dynamic angle',
'standing','sitting','lying','kneeling','squatting','leaning forward','leaning back',
'arms up','arms behind head','arms behind back','hand on hip','crossed arms',
'on all fours','spread arms','reaching out',
'portrait','close-up','medium shot','wide shot','fisheye',
'full body','upper body','lower body','waist up','knees up',
'smile','grin','smirk','blush','shy','serious','expressionless','surprised',
'happy','sad','angry','embarrassed','seductive','sleepy','crying','pout'
],
// ── キャラ属性 ──
chara_attr:[
// デレ系
'tsundere','yandere','kuudere','dandere','deredere',
'himedere','kamidere','sadodere','masodere','dorodere',
'bakadere','hajidere','hinedere','utsudere','goudere',
// 明るい・ポジティブ系
'cheerful','energetic','optimistic','bubbly','lively',
'playful','mischievous','carefree','easygoing','friendly',
'kind','gentle','warm','caring','affectionate',
'innocent','pure','naive','childlike','sincere',
// クール・ミステリアス系
'serious','stoic','cold','aloof','distant',
'mysterious','enigmatic','dark','brooding','gloomy',
'melancholic','lonely','sad','quiet','reserved',
// 強気・自信系
'confident','arrogant','prideful','dominant','assertive',
'bold','daring','fearless','brave','courageous',
'charismatic','leader type','ambitious',
// 内向き・弱気系
'shy','timid','nervous','anxious','submissive',
'clumsy','airheaded','ditzy','awkward','insecure',
'sensitive','emotional','crybaby',
// 特殊感情・執着系
'obsessive','possessive','jealous','yandere-like',
'protective','devoted','loyal','clingy',
'sadistic','masochistic','manipulative','deceptive',
// 知的・個性系
'genius','intellectual','bookworm','nerdy','otaku',
'mature','wise','calm','rational','logical',
'creative','artistic','musical',
// 活発・アクション系
'tomboyish','athletic','sporty','competitive','reckless',
'impulsive','hot-headed','stubborn','rebellious',
'delinquent','rough','wild',
// 女の子らしい・華やか系
'girly','elegant','graceful','refined','ladylike',
'ojou-sama','princess-like','flirtatious','charming','seductive',
// ロール・立場属性
'onee-san type','imouto type','senpai','kouhai',
'childhood friend','transfer student','honor student',
'idol','gyaru','kunoichi type',
// 特殊設定
'amnesiac','time traveler','reincarnated',
'half-human','last of her kind','chosen one',
'cursed','haunted','possessed'
],
// ── 外見: 髪色 ──
appear_haircolor:[
// 無彩色
'white hair','black hair','silver hair','gray hair','platinum hair',
// 金・茶系
'blonde hair','golden hair','dark blonde hair','strawberry blonde hair',
'brown hair','light brown hair','dark brown hair','chestnut hair','auburn hair',
// 暖色系
'red hair','dark red hair','wine red hair','cherry red hair','crimson hair',
'orange hair','amber hair','copper hair','caramel hair','peach hair',
'pink hair','hot pink hair','pale pink hair','rose pink hair','magenta hair',
// 寒色系
'blue hair','dark blue hair','navy blue hair','sky blue hair','royal blue hair',
'aqua hair','teal hair','turquoise hair','cyan hair',
'green hair','dark green hair','mint green hair','lime green hair','forest green hair',
'purple hair','dark purple hair','lavender hair','lilac hair','violet hair','indigo hair',
// 特殊・複色
'multicolored hair','gradient hair','streaked hair','two-tone hair',
'rainbow hair','ombre hair','highlighted hair','frosted tips',
'roots showing','dip-dyed hair','split color hair','tricolor hair'
],
// ── 外見: 髪型 ──
appear_hairstyle:[
// 長さ
'long hair','medium hair','short hair','very long hair','very short hair',
'extra long hair','floor-length hair','waist-length hair','shoulder-length hair',
'neck-length hair','chin-length hair',
// ポニーテール・テール系
'ponytail','high ponytail','low ponytail','side ponytail',
'twintails','asymmetrical twintails','low twintails','high twintails','pigtails',
// アップスタイル
'hair bun','double bun','top knot','hair up','chignon',
'half up','half updo','crown braid','odango','space buns',
// 三つ編み系
'braid','braided hair','twin braids','french braid','dutch braid',
'fishtail braid','side braid','over-shoulder braid',
// 前髪・分け目
'bangs','blunt bangs','side swept bangs','curtain bangs','see-through bangs',
'no bangs','parted hair','middle part','side part','hime cut',
// 質感・形状
'straight hair','wavy hair','curly hair','loose curls','spiral curls',
'ringlets','kinked hair','messy hair','bedhead','fluffy hair',
// カットスタイル
'bob cut','pixie cut','undercut','shaggy hair','layered hair',
'ahoge','drill hair','sidelocks','hair down',
// アクセサリー
'hair flower','hair ribbon','hair ornament','hair clip','hairpin',
'barrette','hair tie','scrunchie','headband','hair bow',
'hair band','flower crown','tiara on hair','hair beads','butterfly hair clip'
],
// ── 外見: 目 ──
appear_eye:[
'red eyes','blue eyes','green eyes','purple eyes','gold eyes','silver eyes',
'brown eyes','heterochromia','aqua eyes','pink eyes','yellow eyes','white eyes',
'orange eyes','black eyes','gray eyes','teal eyes','violet eyes',
'glowing eyes','empty eyes','sparkling eyes','half-closed eyes','closed eyes',
'wide eyes','narrow eyes','bedroom eyes','innocent eyes',
'bags under eyes','dark circles','heavy eyelids',
'thick eyebrows','thin eyebrows','long eyelashes','false eyelashes','white eyelashes',
'eye patch','glasses','sunglasses','monocle','colored contact lenses',
'double eyelid','monolid','sharp eyes','soft eyes',
'beautiful eyes','detailed eyes','distinct pupils'
],
// ── 外見: 顔・鼻・口 ──
appear_face:[
'round face','oval face','heart-shaped face','square jaw','pointed chin',
'soft features','angular features','sharp jaw',
'freckles','scar','mole','mole under eye','mole on cheek',
'mole on neck','mole on breast','birthmark','blush stickers','face paint',
'button nose','small nose','upturned nose','pointy nose','flat nose',
'thin lips','plump lips','full lips','small mouth','wide mouth',
'fangs','vampire fangs','tongue out','tongue piercing','lip piercing',
'beauty mark','smug','ahegao','pout','open mouth','closed mouth'
],
// ── 外見: 体の作り ──
// ── 外見: 肌の色 ──
appear_skin:[
// 白系・明るい肌
'pale skin','fair skin','white skin','porcelain skin','milky skin','translucent skin',
'albino','white character',
// 小麦・健康肌
'tan skin','tanned skin','healthy skin','olive skin','light-brown skin','sun-kissed skin',
'tanned','sunburn','bronzed skin',
// 黒・濃い肌
'dark skin','dark-skinned','black skin','ebony skin',
// 質感・状態
'smooth skin','glowing skin','soft skin','rosy skin','dewy skin','satin skin','matte skin',
'freckled skin','glistening skin','sweaty skin'
],
// ── 外見: 体形 ──
appear_bodyshape:[
// 細身・スリム
'slim body','slender','thin','petite body','skinny','lean body',
// 筋肉・引き締まり
'athletic','muscular','abs','toned body','fit body','broad shoulders',
// 曲線・ふっくら
'curvy','plump','chubby','soft body','voluptuous','plus size',
// バスト
'large breasts','medium breasts','small breasts','flat chest','busty',
// ウエスト・ヒップ
'wide hips','narrow waist','hourglass figure','pear figure','big butt','flat butt',
// 体の細部
'navel','belly button','visible collarbone','visible ribs','dimples of venus',
// 脚
'long legs','short legs','thick thighs','slim legs','thigh gap'
],
// ── 外見: アクセサリー ──
appear_accessory:[
// ジュエリー
'necklace','earrings','bracelet','ring','choker','collar',
'anklet','armband','wristband','body chain','belly ring',
'tiara','crown',
// 頭・髪
'headband','hair clip','hair ribbon','hair bow','hair pin','scrunchie',
'veil','hair accessory',
// 眼鏡・帽子
'glasses','sunglasses','hat','cap','beret','hood',
// 手袋・靴下
'gloves','stockings','fishnets','thigh-high socks','knee-high socks',
// 体・マーク
'tattoo','piercing','body piercing','scar on body','mask',
// その他
'badge','ribbon','brooch','watch','wristwatch'
],
// ── 外見: 身長 ──
appear_height:[
'tall','short','average height','very tall','extremely tall',
'petite','chibi','towering','height difference',
'small stature','large stature','tall girl','short girl',
'tall boy','short boy','loli','shota','adult body','mature',
'same height','shorter than viewer','taller than viewer'
],
// ── 外見: 特殊外見 ──
appear_special:[
'elf ears','cat ears','dog ears','fox ears','wolf ears',
'rabbit ears','bear ears','dragon ears','animal ears','kemonomimi',
'horns','dragon horns','oni horns','demon horns','antlers','halo',
'angel wings','demon wings','wings','tail','cat tail','fox tail',
'wolf tail','dragon tail','fluffy tail','multiple tails',
'demon girl','angel','vampire','succubus','witch','elf','half-elf',
'ghost','zombie','robot ears','mechanical parts','cyborg',
'scales','fur','monster girl','dragon girl','naga','lamia',
'glowing markings','tribal markings','runes on body','dark sclera'
],
// ── 衣装: 日常・カジュアル・スポーツ ──
costume_casual:[
// トップス
't-shirt','crop top','tank top','camisole','tube top','off-shoulder top','halter top',
'hoodie','oversized hoodie','zip-up hoodie','crop hoodie','sweatshirt',
'sweater','turtleneck sweater','knit sweater','cardigan',
'polo shirt','blouse','button-up shirt','flannel shirt','denim shirt','sleeveless shirt',
'bodysuit',
// ボトムス
'jeans','skinny jeans','wide-leg pants','shorts','hot pants','denim shorts',
'cargo pants','sweatpants','yoga pants','leggings','track pants',
// スカート
'mini skirt','midi skirt','maxi skirt','pleated skirt','denim skirt',
'pencil skirt','wrap skirt','A-line skirt','tiered skirt','ruffled skirt',
// ワンピース
'sundress','casual dress','shirt dress','wrap dress','slip dress',
'mini dress','maxi dress','off-shoulder dress','strapless dress','halter dress','backless dress',
// アウター
'denim jacket','leather jacket','bomber jacket','varsity jacket','trench coat',
'peacoat','parka','windbreaker','raincoat','fur coat','faux fur coat','oversized coat','blazer',
// セット
'overalls','dungarees','romper','jumpsuit',
// スポーツ
'gym clothes','sportswear','sports bra','running shorts','track suit',
'one-piece swimsuit','competition swimsuit',
'gymnastics leotard','volleyball uniform','basketball jersey','soccer jersey',
'tennis outfit','baseball uniform','martial arts gi','boxing shorts',
'cycling shorts','ski suit','wetsuit','equestrian outfit'
],
// ── 衣装: 制服・礼装・伝統 ──
costume_uniform:[
// 学校制服
'school uniform','sailor uniform','blazer uniform','gakuran',
'summer school uniform','winter school uniform','gym uniform','graduation hakama',
// 医療・科学
'nurse uniform','doctor coat','lab coat','scrubs','surgeon gown','pharmacist coat',
// 法律・軍事・安全
'police uniform','military uniform','army uniform','navy uniform','air force uniform',
'firefighter uniform','security guard uniform','SWAT uniform','camouflage uniform','soldier uniform',
// サービス・接客
'flight attendant uniform','pilot uniform','chef uniform','waiter uniform','waitress uniform',
'barista apron','maid uniform','butler uniform','hotel staff uniform','postal worker uniform',
// ビジネス
'business suit','office lady outfit','lawyer suit','judge robe',
'construction worker outfit','mechanic uniform','factory worker uniform',
// フォーマル・礼装
'tuxedo','formal suit','black tie outfit','morning coat',
'evening gown','ball gown','cocktail dress','prom dress','red carpet gown',
// ウェディング
'wedding dress','bridal dress','bridesmaid dress','bridal kimono',
// 日本伝統
'kimono','furisode','yukata','hakama','miko outfit','geisha outfit',
'haori','tomesode','houmongi','komon','kunoichi outfit',
// アジア伝統
'hanbok','qipao','cheongsam','tang suit','ao dai',
// 宗教
'monk robe','buddhist monk robe','priest outfit','nun outfit','bishop robe',
'shinto priest outfit','crusader armor',
// ロリータ
'lolita dress','gothic lolita','sweet lolita','classic lolita',
'punk lolita','hime lolita','sailor lolita',
// ダンス・ステージ
'ballet tutu','ballet leotard','ballroom dress','flamenco dress','figure skating outfit',
'belly dancer outfit','hula outfit','stage costume','idol costume','cheerleader outfit'
],
// ── 衣装: ファンタジー・SF・特殊 ──
costume_fantasy:[
// 鎧・重装備
'knight armor','full plate armor','chainmail armor','leather armor','dark knight armor',
'paladin armor','valkyrie armor','dragon armor','demon armor','battle armor',
'royal armor','holy knight armor','battle bikini armor',
// 魔法使い系
'wizard robe','mage robe','witch outfit','warlock robe','sorcerer robe',
'enchanter robe','necromancer robe','summoner robe','druid robe','oracle robe',
'magical girl outfit','mahou shoujo',
// 戦士・盗賊
'ranger outfit','archer outfit','rogue outfit','assassin outfit','thief outfit',
'berserker armor','barbarian outfit','warrior outfit','mercenary outfit',
'adventurer outfit','hero outfit','adventurer cloak',
// 特殊・超自然
'angel outfit','fallen angel outfit','demon outfit','vampire outfit','succubus outfit',
'ghost outfit','zombie outfit','fairy outfit','elf outfit',
'exorcist outfit','onmyoji outfit','shaman outfit',
// 歴史・民族
'samurai armor','viking armor','gladiator outfit','roman armor','greek warrior outfit',
'spartan armor','egyptian outfit','pharaoh outfit',
'medieval dress','renaissance dress','baroque dress','victorian dress','rococo dress',
'ancient greek dress','native american outfit',
// SF・サイバーパンク
'space suit','astronaut suit','sci-fi armor','cyberpunk outfit','neon cyberpunk outfit',
'android outfit','power armor','tactical gear','hazmat suit','mecha suit',
'futuristic uniform','hacker outfit',
// コスプレ・イベント
'bunny suit','bunny girl outfit','cat girl outfit',
'santa outfit','christmas outfit','halloween costume',
'pirate outfit','cowboy outfit','western outfit',
'clown outfit','ringmaster outfit','magician outfit','circus outfit','court jester outfit',
'steampunk outfit','gothic outfit',
// ファンタジー王族
'princess dress','queen gown','noble outfit','royal court dress',
'fairy tale princess','dark queen outfit','elf queen outfit',
// ランジェリー・寝巻き
'lingerie','lace lingerie','corset','babydoll','nightgown',
'silk pajamas','bathrobe','negligee'
],
// ── 絵柄スタイル ──
style:[
'kyoto animation','production i.g','ufotable','wit studio','cloverworks',
'a-1 pictures','trigger','mappa','shaft','toei animation','studio ghibli'
],
// ── Animagine ポジティブ ──
animagine:[
'masterpiece','high score','great score','absurdres','very aesthetic',
'official art','clean lineart',
'cel shading','flat color',
'crisp detail','highres',
// 髪の散らばりを抑える
'tidy hair','neat hair','smooth hair','well-groomed hair',
'sleek hair','perfect hair','styled hair','hair in place'
],
// ── Pony ポジティブ ──
pony:[
'score_9','score_8_up','score_7_up','score_6_up','score_5_up','score_4_up',
'source_anime','source_furry','source_cartoon','source_pony',
'rating_safe','rating_questionable','rating_explicit',
'best quality','high quality','detailed','intricate details','sharp focus','masterpiece','absurdres','highres'
],
// ── Animagine ネガティブ ──
neg_animagine:[
'lowres','bad anatomy','bad hands','text','error','missing fingers','extra digits',
'fewer digits','cropped','worst quality','low quality','low score','bad score',
'average score','signature','watermark','username','blurry',
'sketch','doodle','rough draft',
'messy lines','rough lines',
'traditional media','watercolor','pencil',
// 背景を除去したい場合
'background','scenery','building','nature','detailed background',
// フレーミング除外
'head out of frame',
// 目の形状を除外
'no sclera','colored sclera','solid eyes',
// 髪の散らばりを防ぐ
'frizzy hair','messy hair','flyaway hair','bad hair day',
'ahoge','stray hair','unruly hair','wild hair'
],
// ── ANI ネガティブ: 肌の色制御 ──
neg_ani_skin:[
// 色白にしたい場合に除外
'dark skin','tanned skin','sun-kissed',
// 黒肌にしたい場合に除外
'pale skin','fair skin','white skin'
],
// ── Pony ネガティブ ──
neg_pony:[
'score_1','score_2','score_3','score_4',
'worst quality','low quality','normal quality','lowres','jpeg artifacts','compression artifacts','blurry','noisy','pixelated',
'bad anatomy','bad hands','bad feet','extra limbs','missing limbs','fused body parts','extra fingers','missing fingers','deformed','ugly','disfigured','malformed','bad proportions','cloned face','long neck',
'source_pony','source_furry','source_cartoon','source_real','rating_safe','rating_questionable',
'sketch','3d render','realistic','photo','photograph','flat color','monochrome','grayscale','out of frame','cropped','watermark','text','duplicate','artist name','patreon logo','poorly drawn'
],
// ── NSFW ──
nsfw_rating:[
'nsfw','explicit','uncensored','no censor','nude','completely nude',
'rating_explicit','rating_questionable','rating_safe',
'mosaic censorship','bar censor','partially censored'
],
nsfw_body_f:[
// 胸
'breasts','small breasts','medium breasts','large breasts','huge breasts','gigantic breasts',
'flat chest','perky breasts','sagging breasts','bouncing breasts','breast press',
'cleavage','deep cleavage','underboob','sideboob','topless','bare breasts',
// 乳首
'nipples','erect nipples','inverted nipples','puffy nipples','dark nipples',
'pink nipples','large areolae','areolae','nipple piercing',
// 女性器
'pussy','wet pussy','dripping pussy','spread pussy','puffy pussy',
'hairy pussy','shaved pussy','visible pussy','pussy juice',
'labia','labia minora','labia majora','clitoris','clitoral hood','vaginal opening',
// お尻・下半身
'ass','big ass','round ass','bubble butt','ass grab','butt crack',
'thick thighs','inner thigh','wide hips','big hips',
// その他
'navel','belly button','armpits','exposed armpits','navel piercing'
],
nsfw_body_m:[
// 男性器
'penis','erect penis','flaccid penis','huge cock','thick cock','long cock',
'small penis','veiny penis','throbbing cock','circumcised','uncircumcised','foreskin',
'balls','testicles','scrotum','taint',
// 体型
'masculine body','muscular','chest hair','male pubic hair','pubic hair'
],
nsfw_position:[
// 正常位系
'missionary','legs up missionary','missionary pov',
// 後背位系
'doggy style','prone bone','face down ass up','from behind standing',
// 騎乗系
'cowgirl position','reverse cowgirl','straddling','sitting on lap',
// 立位系
'standing sex','wall sex','against wall','lifted and penetrated','carrying sex',
// 特殊体位
'mating press','pile driver','amazon position','lotus position',
'wheelbarrow position','suspended congress','spoon position',
// 開脚・脚系
'spread legs','legs up','legs over shoulders','legs behind head','leg lock',
// 屈み系
'bent over','bent over desk','bent over table','pressed against wall',
// 寝位系
'lying on back','lying on stomach','lying on side','on all fours sex',
// その他
'riding','grinding','reverse sitting','face sitting position',
'pov sex','from above pov'
],
nsfw_vaginal:[
// ペニス挿入
'vaginal','vaginal sex','vaginal penetration','deep penetration','shallow penetration',
'womb penetration','cervix penetration',
// 指・手
'fingering','finger insertion','two fingers','three fingers','knuckle deep','fisting',
// 器具
'sex toy','dildo','vibrator','wand vibrator','double dildo',
// 外部刺激
'rubbing','clitoral stimulation','grinding on penis',
// 自慰
'masturbation','fingering herself','solo masturbation','mutual masturbation',
// その他
'insertion','object insertion','cervix view'
],
nsfw_oral:[
// フェラチオ
'fellatio','blowjob','deepthroat','irrumatio','facefuck','throat fuck',
'penis licking','licking shaft','ball licking',
// クンニ
'cunnilingus','face sitting','pussy licking','clit licking',
// 69
'69',
// 口の描写
'tongue out','saliva','saliva string','drooling on penis','mouth full',
'bulge in cheek','gagging','teary eyes from oral',
'cum in mouth','swallowing cum','spitting out cum',
// 乳首舐め
'nipple licking','nipple sucking','breast licking',
// その他
'sucking fingers','oral sex','deep kiss','sloppy kiss'
],
nsfw_anal:[
// 挿入
'anal','anal sex','anal penetration','anal insertion','deep anal','anal pov',
// フィニッシュ
'anal creampie','cum in ass',
// ダブル/トリプル
'double penetration','dp','triple penetration',
// ペギング
'pegging','strapon anal',
// 指
'finger in ass','anal fingering','two fingers in ass',
// 器具
'butt plug','anal beads','plug tail','object in ass',
// 開肛
'ass spread','spread ass','gaping','gaping ass','prolapse',
// 舐め
'rimjob','ass licking','ass worship'
],
nsfw_handjob:[
// 手コキ
'handjob','stroking','two-handed handjob','reverse handjob',
// フットジョブ
'footjob','foot on penis','toes on penis','sole on penis',
// 太もも
'thigh sex','thigh job','between thighs',
// 脇
'armpit sex','armpit job',
// その他
'male masturbation','stroking cock','mutual masturbation','jerking off'
],
nsfw_paizuri:[
// パイズリ
'paizuri','titjob','breast sex','tit fuck','paizuri from below',
'paizuri pov','looking down at viewer paizuri','clothed paizuri',
// バリエーション
'paizuri and blowjob','double paizuri','small breast paizuri','huge breast paizuri',
// 乳首関連
'nipple play','nipple rub','nipple pinch','nipple twist',
'nipple stimulation','breast groping','squeezing breasts'
],
nsfw_cum:[
// 体外射精
'cum','cumshot','facial','cum on face','cum on tongue','cum on hair',
'cum on breasts','cum on body','cum on stomach','cum on back',
'cum on thighs','cum on feet','cum on ass',
// 中出し
'creampie','vaginal creampie','cum inside','womb full of cum',
// 量・演出
'multiple cumshots','massive cumshot','cum overflow','cum dripping','dripping cum',
'cum string','cum trail','cum pool','sticky cum',
// 顔・口
'bukakke','covered in cum','messy cum'
],
nsfw_squirt:[
// 潮吹き
'squirting','female ejaculation','squirt','gushing',
'squirt on face','squirt in mouth',
'massive squirt','squirt dripping','squirt spray',
'ahegao squirt','orgasm squirt','squirt from fingering',
'multiple squirts','wet spot','soaked bed'
],
nsfw_reaction:[
// アヘ顔系
'ahegao','rolling eyes','eyes rolled back','orgasm face','climax face',
// 声・息
'moaning','mouth open','panting','heavy breathing','gasping',
'crying out','screaming in pleasure','muffled moan',
// 顔の変化
'drooling','blushing','full body blush','sweating','flushed face',
// 涙
'crying','tears','tears of pleasure','teary eyes',
// 体の変化
'shaking','trembling','twitching','convulsing','body spasm',
'in heat','goosebumps',
// 精神状態
'mind break','blank stare','dazed','lustful look',
'satisfied expression','ecstasy','euphoria','pleasure expression',
// 抑制・後
'embarrassed moan','trying to suppress moaning','biting lip','biting sleeve',
'ahegao aftermath','dazed after orgasm'
],
nsfw_costume:[
// 脱衣
'naked','topless','bottomless','completely nude',
'undressing','clothes torn','ripped clothes',
'clothes pulled aside','shirt lift','skirt lift','dress lift',
'panties around ankles','panties pulled down','bra pulled down','bra removed',
// 下着
'lingerie','bra','panties','thong','g-string','bikini','micro bikini',
'side-tie panties','crotchless panties','crotchless lingerie',
'sexy underwear','sheer lingerie',
// 特殊下着
'garter belt','garter straps','stockings','thigh-highs','fishnet stockings',
'fishnet bodysuit','corset','bodystocking','see-through lingerie',
// 特殊コスプレ
'maid','nurse','bunny suit','catsuit','latex','leather outfit',
'fishnet outfit','bikini armor',
// 制服・水着
'school uniform','sailor uniform','gym uniform','swimsuit','one-piece swimsuit',
// セクシー衣装
'sexy santa','sexy witch','kimono','yukata open',
// 透け・濡れ
'see-through','wet clothes','nude filter',
// CFNM
'clothed female nude male','clothed sex'
],
nsfw_bondage:[
// ロープ
'bondage','shibari','rope bondage','hogtied','suspension bondage',
// 手足拘束
'handcuffed','wrist cuffs','ankle cuffs','bound wrists','bound ankles',
'bound arms','arms bound behind back',
// 拘束先
'tied to chair','tied to bed','spread eagle',
// 体全体
'restrained','immobilized','spread to bed',
// 目隠し・猿ぐつわ
'blindfold','ballgag','bit gag','ring gag','tape gag','mouth stuffed',
// 首輪・鎖
'collar','slave collar','leash','pulled by leash',
'chains','chain bondage','ankle chain',
// プレイ
'vibrator bondage','forced orgasm','orgasm denial','edging','overstimulation bondage'
],
nsfw_yuri:[
// 行為
'yuri','lesbian sex','girl on girl','female on female',
'tribadism','scissoring','grinding together',
// 器具
'strap-on sex','double-ended dildo','dildo sharing',
// キス・애무
'lesbian kiss','girls kissing','deep kiss girls','tongue kiss',
'breast fondling girls','mutual touching','girls caressing',
// クンニ・指
'girls fingering','mutual fingering','cunnilingus yuri',
// 感情
'romantic yuri','tender yuri','passionate yuri',
// 複数
'multiple girls sex','yuri threesome','girls orgy'
],
nsfw_futa:[
// 基本
'futa','futanari','dickgirl',
// 組み合わせ
'futa on female','futa on male','futa on futa','female on futa',
// 行為
'futa sex','futa blowjob','futa handjob','futa paizuri',
'futa penetration','futa creampie','futa cumshot',
// 外見
'futa bulge','futa erection','futa balls',
'huge futa cock','small futa cock',
// その他
'futa masturbation','futa solo'
],
nsfw_monster:[
// 触手
'tentacle','tentacle sex','tentacle in pussy',
'tentacle in mouth','tentacle in ass','multiple tentacles',
'tentacle wrap','restrained by tentacle','tentacle creampie',
// モンスター
'monster sex','orc','goblin sex','creature sex','beast sex',
'monster penetration','monster cock','non-human penis',
// 機械・その他
'machine sex','mechanical penetration','robot sex','android sex',
'plant sex','vine sex','alien sex'
],
nsfw_scenario:[
// NTR・修羅場
'ntr','netorare','cuckolding','wife sharing',
'cheating','affair','secret sex','forbidden love',
// 支配・服従
'femdom','maledom','submission','domination','bdsm',
'rough sex','degradation','humiliation',
// 同意・強制
'consensual','non-consensual implication','reluctant','forced','coercion',
// 年齢差
'age difference','older man younger woman','older woman younger man',
'onee-shota','shotacon implication','teacher and student',
// 見せ・見られ
'exhibitionism','voyeurism','public sex','sex in public',
'caught having sex','watched','hidden camera implication',
// 集団
'gangbang','orgy','threesome','foursome','harem','reverse harem',
// 同性
'yaoi','male on male','bara',
// 状況
'drunk sex','sleepy sex','somnophilia implication','morning sex',
'one night stand','quickie','first time','virgin','defloration'
],
nsfw_fetish:[
// 足フェチ
'foot fetish','barefoot','soles','licking feet','smelling feet','sucking toes',
'wrinkled soles','foot worship','tickling feet','dirty feet','feet in face',
'foot lick','sole lick','toe lick','smelling soles','foot sniff',
// 脇フェチ
'armpit fetish','licking armpits','armpit sniffing','sweaty armpits','underarm licking',
'smelling armpits','armpit worship',
// お腹・へそフェチ
'belly fetish','navel fetish','navel licking','stomach kissing','navel worship',
'belly button licking','tummy display','belly rub',
// うなじ・首フェチ
'nape fetish','neck licking','throat kissing','nape licking','neck biting',
'collar bone licking','back of neck',
// 耳フェチ
'ear licking','ear nibbling','ear whispering','ear bite','earlobe lick',
// 手・指フェチ
'hand fetish','finger licking','finger sucking','gloves fetish','long fingernails',
'nail fetish','ring fetish','hand licking',
// ふとももフェチ
'thigh fetish','inner thigh','thigh gap','thick thighs worship','thigh press',
'thigh kiss','thigh lick','thigh sniff','between thighs','thigh squeeze',
// お尻フェチ
'butt fetish','spanking','ass slap','ass worship','butt sniff',
'ass massage','ass bite','hip worship',
// 乳首フェチ
'nipple fetish','nipple torture','nipple clamp','nipple pull',
'nipple chain','nipple tickle','nipple tweak','erect nipples worship',
// 汗・においフェチ
'smell fetish','musk','body odor fetish','sweating','glistening sweat',
'sweat droplets','sweaty body','used panties','panty sniffing',
// 唾液フェチ
'saliva fetish','drool','drooling','saliva play','spit fetish',
'tongue display','lip fetish','wet tongue','licking lips',
// ニーハイ・靴下フェチ
'thigh-high fetish','sock fetish','stocking fetish','knee-high socks',
'ankle socks','bare legs','leg worship','fishnet fetish',
// めがねフェチ
'glasses fetish','megane','adjusting glasses','over glasses',
// 制服・衣装フェチ
'uniform fetish','maid fetish','nurse fetish','school uniform fetish',
'gym clothes fetish','swimsuit fetish','latex fetish',
// 背中フェチ
'back view','exposed back','spine view','dimples of venus',
'back muscles','back worship','back lick'
],
nsfw_pose:[
// 誘惑・見せポーズ
'seductive pose','presenting','ass presenting','spread pussy display',
'showing off body','come hither','inviting pose','alluring pose',
// M字開脚系
'legs spread wide','m-shaped legs','lying spread legs',
'spread legs lying','open legs display',
// 仰向け
'lying on back lewdly','legs in air','lying with legs apart',
'supine lewd pose','back on bed',
// うつ伏せ
'lying face down lewd','prone lewd','ass up lying','face down butt up',
'lying prone seductive',
// 横向き
'lying on side lewd','side lying spread','side pose seductive',
// 正座・お座り
'seiza lewd','sitting spread legs','japanese sitting lewd',
'w-sit','floor sitting spread','kneeling spread',
// 立ちポーズ
'standing spread legs','arms up naked','stretching nude',
'wall lean seductive','hip thrust','arched back standing',
// しゃがみ
'squatting lewd','crouching seductive','squat spread',
'crouching display','crouching naked',
// 四つん這い
'all fours presenting','on all fours lewd','ass up all fours',
'crawling seductive','doggy display',
// 騎乗系ポーズ
'mounting pose','straddling pose','riding position display',
// 壁・床
'pressed against wall pose','floor lying display','back against wall',
'wall spread','pinned pose',
// POV系
'looking up at viewer','looking down at viewer',
'from pov above','pov looking down','eye contact pov',
// くびれ・体強調
'waist display','hip emphasis','body curve display',
'arched back lying','chest out pose',
// 脚上げ
'raised hips','lifting one leg','leg raised high',
'one leg up','high kick pose',
// 開脚
'full split','side split','legs up high','maximum spread',
// 抱き系
'hugging pillow','body pillow hug','self-hug seductive',
// 起き上がり
'sitting up seductive','getting up pose','stretching awake lewd'
],
nsfw_misc:[
// 場所
'bathroom sex','locker room sex','hot spring sex','car sex',
'outdoor sex','floor sex','against glass','glory hole','phone sex',
// フェチ
'foot fetish','licking feet','feet worship','sole fetish','toe sucking',
'armpit fetish','licking armpits','belly fetish','navel licking',
'smell fetish','used panties','sweaty body','musk','body odor fetish',
// アフター
'after sex','morning after','afterglow','post-orgasm','ahegao aftermath',
'exhausted after sex','well-used',
// 特殊
'lactation','breast milk','milking','pregnant','impregnation','breeding',
'womb tattoo','cervix tattoo',
// 視点
'pov','first person view',
// 内部
'internal view','x-ray','womb view','cross section',
// その他
'multiple orgasms','overstimulation','too much pleasure',
'mind control sex','hypnosis sex'
],
// ========== 実写専用プリセット ==========
// 写真品質
photo_quality: [
'photorealistic','hyperrealistic','ultra realistic','RAW photo',
'DSLR photo','mirrorless camera photo','professional photography',
'portrait photography','street photography','fashion photography',
'cinematic photography','lifestyle photography',
'8k uhd','4k resolution','full HD','high resolution',
'sharp focus','bokeh','shallow depth of field','deep depth of field',
'35mm film','50mm lens','85mm portrait lens','telephoto lens',
'Canon EOS R5','Sony α7 III','Nikon Z9',
'film grain','analog film','high ISO','long exposure',
'studio flash','ring light','natural light photo'
],
// 写真ネガティブ
photo_neg: [
'drawn','anime','illustration','cartoon','painting','sketch','watercolor',
'CGI','3D render','3D model','artificial','plastic skin','doll-like','wax figure',
'bad anatomy','deformed','distorted','disfigured','mutant','extra limbs',
'extra fingers','missing fingers','fused fingers','long neck',
'blurry','out of focus','motion blur','noise','overexposed','underexposed',
'low contrast','flat lighting','harsh shadows',
'worst quality','low quality','bad quality','poor quality','jpeg artifacts',
'text','watermark','signature','border','frame','logo'
],
// 被写体
photo_subject: [
'1 girl','1 woman','1 boy','1 man','2 girls','couple','group of people',
'Japanese girl','Japanese woman','Japanese schoolgirl','high school girl',
'college student','office lady','young woman','teenager',
'solo','alone','with others',
'looking at viewer','looking away','looking back','eyes closed',
'smiling','serious expression','shy','confident','natural expression',
'standing','sitting','walking','running',
'from behind','from side','from above','from below',
'full body','upper body','half body','close-up portrait','face only'
],
// ロケーション・背景
photo_location: [
// 屋外
'outdoors','street','city street','urban','downtown','suburban',
'park','garden','riverside','bridge','rooftop',
'school building exterior','school gate','school courtyard',
'train station','on the train','bus stop','shopping mall',
'beach','seaside','mountain path','forest','nature',
// 屋内
'indoors','cafe','coffee shop','restaurant','library','classroom',
'school hallway','convenience store','bedroom','living room',
// 背景
'white background','blurred background','bokeh background',
'simple background','plain background','out of focus background',
'overcast sky','clear sky','night sky','golden hour sky'
],
// 制服・学校(英語プロンプトより)
// ※元プロンプト和訳: 後ろ姿の日本人女子高校生。ロングウェーブブラウンヘア。
// 黒ブレザー、暗いチェック柄プリーツミニスカート、黒スパンデックスショーツ(下に着用)、
// 白ルーズソックス(コギャルスタイル)、ブラウンローファー、
// 明るいピンクのボストンバッグ(肩紐付き)
photo_uniform: [
// 元プロンプトの各要素
'Japanese schoolgirl', // 日本人女子高校生
'from behind', // 後ろ姿
'back view', // 後ろ姿(別表現)
'long wavy hair', // ロングウェーブヘア
'brown hair', // ブラウンヘア
'long wavy brown hair', // ロングウェーブブラウンヘア
'black blazer', // 黒ブレザー
'navy blazer', // ネイビーブレザー
'gray blazer', // グレーブレザー
'pleated mini skirt', // プリーツミニスカート
'plaid skirt', // チェック柄スカート
'dark plaid pattern', // 暗いチェック柄
'checkered skirt', // チェックスカート
'spandex shorts under skirt', // スカートの下のスパンデックスショーツ
'black shorts underneath', // 黒ショーツ(下に着用)
'compression shorts', // コンプレッションショーツ
'loose white socks', // 白ルーズソックス
'loose socks', // ルーズソックス
'kogal style socks', // コギャルスタイルソックス
'over-knee socks', // オーバーニーソックス
'brown loafers', // ブラウンローファー
'black loafers', // 黒ローファー
'loafers', // ローファー
'bright pink bag', // 明るいピンクのバッグ
'Boston bag', // ボストンバッグ
'shoulder strap bag', // 肩紐バッグ
'pink school bag', // ピンクのスクールバッグ
// その他の制服要素
'school uniform', // 制服
'Japanese school uniform', // 日本の学校制服
'white dress shirt', // 白ドレスシャツ
'necktie', // ネクタイ
'ribbon bow', // リボン
'sailor uniform', // セーラー服
'sailor collar', // セーラーカラー
'summer uniform', // 夏服
'winter uniform', // 冬服
'gym uniform', // 体育服
'mary jane shoes', // メリージェーンシューズ
'white socks', // 白ソックス
'knee-high socks', // ニーハイソックス
'backpack' // リュックサック
],
// 照明
photo_lighting: [
'natural lighting','soft natural light','harsh sunlight','dappled light',
'studio lighting','ring light','softbox lighting','LED panel light',
'golden hour light','sunset light','sunrise light','blue hour',
'backlit','rim lighting','contre-jour','halo lighting',
'window light','indoor fluorescent light','ambient light',
'overcast sky lighting','cloudy diffuse light',
'dramatic lighting','low key lighting','high key lighting',
'moody lighting','warm light','cool light','neutral light',
'hard shadow','soft shadow','no shadow','long shadow'
]
};
const CHARA_DATA = {
"alice in wonderland": ["alice (alice in wonderland)"],
"bayonetta": ["bayonetta (bayonetta)"],
"black lagoon": ["revy (black lagoon)", "roberta (black lagoon)", "shenhua (black lagoon)"],
"bleach": ["inoue orihime (bleach)", "kuchiki rukia (bleach)", "matsumoto rangiku (bleach)", "nelliel tu odelschwanck (bleach)", "shihouin yoruichi (bleach)", "sui-feng (bleach)"],
"blue archive": ["yuuka (blue archive)", "asuna (blue archive)", "karin (blue archive)"],
"bocchi the rock!": ["gotoh hitori (bocchi the rock!)", "hiroi kikuri (bocchi the rock!)", "ijichi nijika (bocchi the rock!)", "ijichi seika (bocchi the rock!)", "kita ikuyo (bocchi the rock!)", "pa-san (bocchi the rock!)", "yamada ryo (bocchi the rock!)"],
"boku no hero academia": ["ashido mina (boku no hero academia)", "asui tsuyu (boku no hero academia)", "bakugou mitsuki (boku no hero academia)", "hadou nejire (boku no hero academia)", "hagakure tooru (boku no hero academia)", "hatsume mei (boku no hero academia)", "jirou kyouka (boku no hero academia)", "kendou itsuka (boku no hero academia)", "lady nagant (boku no hero academia)", "midnight (boku no hero academia)", "mirko (boku no hero academia)", "mount lady (boku no hero academia)", "toga himiko (boku no hero academia)", "uraraka ochako (boku no hero academia)", "yaoyorozu momo (boku no hero academia)"],
"boku no kokoro no yabai yatsu": ["yamada anna (boku no kokoro no yabai yatsu)"],
"chainsaw man": ["higashiyama kobeni (chainsaw man)", "himeno (chainsaw man)", "makima (chainsaw man)", "mitaka asa (chainsaw man)", "nayuta (chainsaw man)", "power (chainsaw man)", "reze (chainsaw man)", "yoru (chainsaw man)"],
"cyberpunk": ["lucy (cyberpunk)", "rebecca (cyberpunk)"],
"death note": ["amane misa (death note)"],
"doa": ["ayane (doa)", "honoka (doa)", "kasumi (doa)", "lei fang (doa)", "marie rose (doa)", "nyotengu (doa)"],
"doraemon": ["minamoto shizuka (doraemon)"],
"dragon ball": ["android 18 (dragon ball)", "android 21 (dragon ball)", "chi-chi (dragon ball)", "lunch (dragon ball)", "majin android 21 (dragon ball)", "pan (dragon ball)", "videl (dragon ball)"],
"evangelion": ["ayanami rei (evangelion)", "katsuragi misato (evangelion)", "souryuu asuka langley (evangelion)"],
"ff10": ["lulu (ff10)", "rikku (ff10)", "yuna (ff10)"],
"ff13": ["lightning farron (ff13)", "oerba dia vanille (ff13)", "oerba yun fang (ff13)", "serah farron (ff13)"],
"ff7": ["aerith gainsborough (ff7)", "tifa lockhart (ff7)", "tifa lockhart (refined dress) (ff7)", "yuffie kisaragi (ff7)"],
"fma": ["lust (fma)", "riza hawkeye (fma)", "winry rockbell (fma)"],
"frieren": ["frieren (frieren)", "aura (frieren)", "fern (frieren)", "übel (frieren)", "Serie (frieren)"],
"guilty gear": ["bridget (guilty gear)", "dizzy (guilty gear)", "elphelt valentine (guilty gear)", "ramlethal valentine (guilty gear)", "jack-o' valentine (guilty gear)", "may (guilty gear)"],
"gurren lagann": ["yoko littner (gurren lagann)"],
"haruhi suzumiya": ["nagato yuki (haruhi suzumiya)", "suzumiya haruhi (haruhi suzumiya)"],
"hololive": ["gawr gura (hololive)", "houshou marine (hololive)", "usada pekora (hololive)", "amane kanata (hololive)", "hoshimachi suisei (hololive)", "inugami korone (hololive)", "nekomata okayu (hololive)", "omaru polka (hololive)", "sakura miko (hololive)", "shirogane noel (hololive)", "shishiro botan (hololive)", "laplus darknesss (hololive)", "minato aqua (hololive)", "oozora subaru (hololive)", "uruha rushia (hololive)", "todoroki hajime (hololive)"],
"hunter x hunter": ["neferpitou (hunter x hunter)", "shizuku murasaki (hunter x hunter)"],
"idolmaster": ["amami haruka (idolmaster)", "shibuya rin (idolmaster)", "futaba anzu (idolmaster)", "maekawa miku (idolmaster)"],
"jujutsu kaisen": ["ieiri shoko (jujutsu kaisen)", "iori utahime (jujutsu kaisen)", "kugisaki nobara (jujutsu kaisen)", "zen'in maki (jujutsu kaisen)"],
"k-on!": ["akiyama mio (k-on!)", "hirasawa ui (k-on!)", "hirasawa yui (k-on!)", "kotobuki tsumugi (k-on!)", "nakano azusa (k-on!)", "tainaka ritsu (k-on!)"],
"kaguya-sama": ["fujiwara chika (kaguya-sama)", "shinomiya kaguya (kaguya-sama)"],
"kamitsubaki": ["kaf (kamitsubaki)"],
"kemono friends": ["serval (kemono friends)"],
"kill la kill": ["kiryuuin satsuki (kill la kill)", "mankanshoku mako (kill la kill)", "matoi ryuuko (kill la kill)"],
"kimetsu no yaiba": ["daki (kimetsu no yaiba)", "kamado nezuko (kimetsu no yaiba)", "kanroji mitsuri (kimetsu no yaiba)", "kochou kanae (kimetsu no yaiba)", "kochou shinobu (kimetsu no yaiba)", "tsuyuri kanao (kimetsu no yaiba)"],
"kof": ["athena asamiya (kof)", "kula diamond (kof)", "leona heidern (kof)", "shiranui mai (kof)"],
"komi-san": ["komi shouko (komi-san)", "komi shuuko (komi-san)"],
"konosuba": ["aqua (konosuba)", "darkness (konosuba)", "megumin (konosuba)"],
"kusuriya no hitorigoto": ["maomao (kusuriya no hitorigoto)"],
"legend of zelda": ["princess zelda (legend of zelda)"],
"love live!": ["ayase eli (love live!)", "hoshizora rin (love live!)", "koizumi hanayo (love live!)", "kousaka honoka (love live!)", "minami kotori (love live!)", "nishikino maki (love live!)", "sonoda umi (love live!)", "toujou nozomi (love live!)", "yazawa nico (love live!)"],
"lucky star": ["izumi konata (lucky star)"],
"madoka magica": ["akemi homura (madoka magica)", "kaname madoka (madoka magica)", "miki sayaka (madoka magica)", "sakura kyoko (madoka magica)", "tomoe mami (madoka magica)"],
"maidragon": ["elma (maidragon)", "ilulu (maidragon)", "kanna kamui (maidragon)", "kobayashi (maidragon)", "lucoa (maidragon)", "tohru (maidragon)"],
"metroid": ["samus aran (metroid)"],
"mirai nikki": ["gasai yuno (mirai nikki)", "minene uryuu (mirai nikki)", "tsubaki kasugano (mirai nikki)"],
"monogatari": ["araragi karen (monogatari)", "araragi tsukihi (monogatari)", "black hanekawa (monogatari)", "hanekawa tsubasa (monogatari)", "kanbaru suruga (monogatari)", "kiss-shot acerola-orion heart-under-blade (monogatari)", "ononoki yotsugi (monogatari)", "oshino ougi (monogatari)", "sengoku nadeko (monogatari)", "senjougahara hitagi (monogatari)"],
"naruto": ["haruno sakura (naruto)", "hyuuga hinata (naruto)", "mitarashi anko (naruto)", "naruko (naruto)", "tayuya (naruto)", "temari (naruto)", "tenten (naruto)", "tsunade (naruto)", "yamanaka ino (naruto)"],
"needy girl overdose": ["ame-chan (needy girl overdose)", "chouzetsusaikawa tenshi-chan (needy girl overdose)"],
"nier:automata": ["2b (nier:automata)"],
"nijisanji": ["ange katrina (nijisanji)", "higuchi kaede (nijisanji)", "honma himawari (nijisanji)", "inui toko (nijisanji)", "lize helesta (nijisanji)", "sasaki saku (nijisanji)", "tsukino mito (nijisanji)", "ars almal (nijisanji)", "makaino ririmu (nijisanji)", "ryushen (nijisanji)", "sukoya kana (nijisanji)", "suzuhara lulu (nijisanji)", "suzuka utako (nijisanji)", "takamiya rion (nijisanji)", "yuzuki roa (nijisanji)"],
"one piece": ["boa hancock (one piece)", "nami (one piece)", "nico robin (one piece)", "perona (one piece)", "tashigi (one piece)", "uta (one piece)", "yamato (one piece)"],
"one-punch man": ["fubuki (one-punch man)", "tatsumaki (one-punch man)"],
"oreimo": ["aragaki ayase (oreimo)", "kousaka kirino (oreimo)"],
"oshi no ko": ["arima kana (oshi no ko)", "hoshino ai (oshi no ko)", "hoshino ruby (oshi no ko)", "mem-cho (oshi no ko)", "kurokawa akane (oshi no ko)"],
"overlord": ["albedo (overlord)"],
"overwatch": ["d.va (overwatch)", "kiriko (overwatch)", "mei (overwatch)", "mercy (overwatch)", "tracer (overwatch)", "widowmaker (overwatch)"],
"persona": ["aegis (persona)"],
"persona 3": ["takeba yukari (persona 3)"],
"persona 4": ["satonaka chie (persona 4)"],
"persona 5": ["sakura futaba (persona 5)"],
"pokemon": ["Iono (pokemon)", "Lana (pokemon)", "May (pokemon)", "Dawn (pokemon)"],
"re:zero": ["emilia (re:zero)", "ram (re:zero)", "rem (re:zero)"],
"sailor moon": ["sailor moon (sailor moon)", "chibiusa (sailor moon)", "hino rei (sailor moon)", "kaiou michiru (sailor moon)", "kino makoto (sailor moon)", "mizuno ami (sailor moon)"],
"serial experiments lain": ["iwakura lain (serial experiments lain)"],
"shakugan no shana": ["shana (shakugan no shana)"],
"shingeki no kyojin": ["annie leonhardt (shingeki no kyojin)", "mikasa ackerman (shingeki no kyojin)"],
"skullgirls": ["filia (skullgirls)", "valentine (skullgirls)"],
"splatoon": ["inkling girl (splatoon)"],
"spy x family": ["anya (spy x family)", "yor briar (spy x family)"],
"ssss.gridman": ["shinjou akane (ssss.gridman)", "takarada rikka (ssss.gridman)"],
"street fighter": ["chun-li (street fighter)", "cammy white (street fighter)", "ibuki (street fighter)", "juri han (street fighter)", "karin kanzuki (street fighter)", "makoto (street fighter)", "sakura kasugano (street fighter)"],
"super mario": ["bowsette (super mario)"],
"to love-ru": ["konjiki no yami (to love-ru)", "kotegawa yui (to love-ru)", "lala satalin deviluke (to love-ru)", "momo belia deviluke (to love-ru)", "nana aster deviluke (to love-ru)", "sairenji haruna (to love-ru)", "yuuki mikan (to love-ru)"],
"toaru kagaku no railgun": ["misaka mikoto (toaru kagaku no railgun)", "shirai kuroko (toaru kagaku no railgun)", "shokuhou misaki (toaru kagaku no railgun)", "uiharu kazari (toaru kagaku no railgun)"],
"toaru majutsu no index": ["misaka imouto (toaru majutsu no index)"],
"tokyo ghoul": ["kirishima touka (tokyo ghoul)"],
"touhou": ["alice margatroid (touhou)", "chen (touhou)", "cirno (touhou)", "flandre scarlet (touhou)", "fujiwara no mokou (touhou)", "hakurei reimu (touhou)", "hinanawi tenshi (touhou)", "houraisan kaguya (touhou)", "ibuki suika (touhou)", "inaba tewi (touhou)", "inubashiri momiji (touhou)", "izayoi sakuya (touhou)", "kaenbyou rin (touhou)", "kawashiro nitori (touhou)", "kirisame marisa (touhou)", "kochiya sanae (touhou)", "komeiji koishi (touhou)", "komeiji satori (touhou)", "konpaku youmu (touhou)", "konpaku youmu (ghost) (touhou)", "moriya suwako (touhou)", "mystia lorelei (touhou)", "patchouli knowledge (touhou)", "reisen udongein inaba (touhou)", "remilia scarlet (touhou)", "rumia (touhou)", "saigyouji yuyuko (touhou)", "shameimaru aya (touhou)", "yagokoro eirin (touhou)"],
"uma musume": ["agnes tachyon (uma musume)", "daiwa scarlet (uma musume)", "gold ship (uma musume)", "mejiro mcqueen (uma musume)", "silence suzuka (uma musume)", "tokai teio (uma musume)", "special week (uma musume)", "ikuno dictus (uma musume)"],
"utau": ["kasane teto (utau)"],
"uzaki-chan": ["uzaki hana (uzaki-chan)", "uzaki tsuki (uzaki-chan)"],
"violet evergarden": ["violet evergarden (violet evergarden)"],
"vocaloid": ["hatsune miku (vocaloid)", "kagamine rin (vocaloid)", "gumi (vocaloid)", "ia (vocaloid)", "megurine luka (vocaloid)", "meiko (vocaloid)"],
"voiceroid": ["kotonoha akane (voiceroid)", "kotonoha aoi (voiceroid)", "yuzuki yukari (voiceroid)"],
"vspo!": ["tachibana hinano (vspo!)"],
"vtuber": ["cyber girl siro (vtuber)", "inuyama tamaki (vtuber)", "kagura nana (vtuber)", "kaguya luna (vtuber)", "kizuna ai (vtuber)", "mirai akari (vtuber)", "shigure ui (vtuber)"],
"xenosaga": ["kos-mos (xenosaga)"],
"yofukashi no uta": ["nanakusa nazuna (yofukashi no uta)"],
"yu-gi-oh!": ["dark magician girl (yu-gi-oh!)"],
"zenless zone zero": ["anby demara (zenless zone zero)", "ellen joe (zenless zone zero)", "jane doe (zenless zone zero)", "nekomiya mana (zenless zone zero)", "nicole demara (zenless zone zero)", "soldier 11 (zenless zone zero)"]
};
const CHARA_SERIES_NAMES = {
"alice in wonderland": "不思議の国のアリス",
"bayonetta": "ベヨネッタ",
"black lagoon": "BLACK LAGOON",
"bleach": "BLEACH",
"blue archive": "ブルーアーカイブ",
"bocchi the rock!": "ぼっち・ざ・ろっく!",
"boku no hero academia": "僕のヒーローアカデミア",
"boku no kokoro no yabai yatsu": "僕ヤバ",
"chainsaw man": "チェンソーマン",
"cyberpunk": "サイバーパンク",
"death note": "デスノート",
"doa": "Dead or Alive",
"doraemon": "ドラえもん",
"dragon ball": "ドラゴンボール",
"evangelion": "エヴァンゲリオン",
"ff10": "FF10",
"ff13": "FF13",
"ff7": "FF7",
"fma": "鋼の錬金術師",
"frieren": "葬送のフリーレン",
"guilty gear": "GUILTY GEAR",
"gurren lagann": "グレンラガン",
"haruhi suzumiya": "涼宮ハルヒ",
"hololive": "ホロライブ",
"hunter x hunter": "HUNTER×HUNTER",
"idolmaster": "アイドルマスター",
"jujutsu kaisen": "呪術廻戦",
"k-on!": "けいおん!",
"kaguya-sama": "かぐや様",
"kamitsubaki": "神椿市",
"kemono friends": "けものフレンズ",
"kill la kill": "キルラキル",
"kimetsu no yaiba": "鬼滅の刃",
"kof": "KOF",
"komi-san": "コミさん",
"konosuba": "このすば",
"kusuriya no hitorigoto": "薬屋のひとりごと",
"legend of zelda": "ゼルダの伝説",
"love live!": "ラブライブ!",
"lucky star": "らき☆すた",
"madoka magica": "魔法少女まどか☆マギカ",
"maidragon": "小林さんちのメイドラゴン",
"metroid": "メトロイド",
"mirai nikki": "未来日記",
"monogatari": "物語シリーズ",
"naruto": "NARUTO",
"needy girl overdose": "ネコぜとねこと",
"nier:automata": "NieR:Automata",
"nijisanji": "にじさんじ",
"one piece": "ONE PIECE",
"one-punch man": "One-Punch Man",
"oreimo": "俺妹",
"oshi no ko": "【推しの子】",
"overlord": "オーバーロード",
"overwatch": "オーバーウォッチ",
"persona": "ペルソナ",
"persona 3": "ペルソナ3",
"persona 4": "ペルソナ4",
"persona 5": "ペルソナ5",
"pokemon": "ポケモン",
"re:zero": "Re:ゼロ",
"sailor moon": "セーラームーン",
"serial experiments lain": "lain",
"shakugan no shana": "灼眼のシャナ",
"shingeki no kyojin": "進撃の巨人",
"skullgirls": "スカルガールズ",
"splatoon": "スプラトゥーン",
"spy x family": "SPY×FAMILY",
"ssss.gridman": "SSSS.GRIDMAN",
"street fighter": "ストリートファイター",
"super mario": "スーパーマリオ",
"to love-ru": "To LOVEる",
"toaru kagaku no railgun": "超電磁砲",
"toaru majutsu no index": "禁書目録",
"tokyo ghoul": "東京喰種",
"touhou": "東方Project",
"uma musume": "ウマ娘",
"utau": "UTAU",
"uzaki-chan": "宇崎ちゃん",
"violet evergarden": "ヴァイオレット",
"vocaloid": "VOCALOID",
"voiceroid": "VOICEROID",
"vspo!": "ぶいすぽっ!",
"vtuber": "VTuber",
"xenosaga": "ゼノサーガ",
"yofukashi no uta": "よふかしのうた",
"yu-gi-oh!": "Yu-Gi-Oh!",
"zenless zone zero": "ゼンレスゾーンゼロ"
};
const SERIES_READING = {
"alice in wonderland": "ふしぎのくにのありす",
"bayonetta": "べよねった",
"black lagoon": "ぶらっくらぐーん",
"bleach": "ぶりーち",
"blue archive": "ぶるーあーかいぶ",
"bocchi the rock!": "ぼっちざろっく",
"boku no hero academia": "ぼくのひーろー",
"boku no kokoro no yabai yatsu": "ぼくやば",
"chainsaw man": "ちぇーんそーまん",
"cyberpunk": "さいばーぱんく",
"death note": "でーすのーと",
"doa": "でっどおあらいぶ",
"doraemon": "どらえもん",
"dragon ball": "どらごんぼーる",
"evangelion": "えゔぁんげりおん",
"ff10": "えふえふじゅう",
"ff13": "えふえふじゅうさん",
"ff7": "えふえふなな",
"fma": "はがれん",
"frieren": "ふりーれん",
"guilty gear": "きるてぃぎあ",
"gurren lagann": "ぐれんらがん",
"haruhi suzumiya": "はるひすずみや",
"hololive": "ほろらいぶ",
"hunter x hunter": "はんたーはんたー",
"idolmaster": "あいどるますたー",
"jujutsu kaisen": "じゅじゅつかいせん",
"k-on!": "けいおん",
"kaguya-sama": "かぐやさま",
"kamitsubaki": "かみつばき",
"kemono friends": "けものふれんず",
"kill la kill": "きるらきる",
"kimetsu no yaiba": "きめつのやいば",
"kof": "きんぐおふふぁいたーず",
"komi-san": "こみさん",
"konosuba": "このすば",
"kusuriya no hitorigoto": "くすりやのひとりごと",
"legend of zelda": "ぜるだのでんせつ",
"love live!": "らぶらいぶ",
"lucky star": "らきすた",
"madoka magica": "まどかまぎか",
"maidragon": "めいどらごん",
"metroid": "めとろいど",
"mirai nikki": "みらいにっき",
"monogatari": "ものがたり",
"naruto": "なると",
"needy girl overdose": "いんたーねっとおでっせい",
"nier:automata": "にーあ",
"nijisanji": "にじさんじ",
"one piece": "わんぴーす",
"one-punch man": "one-punch man",
"oreimo": "おれいも",
"oshi no ko": "おしのこ",
"overlord": "おーばーろーど",
"overwatch": "おーばーうぉっち",
"persona": "ぺるそな",
"persona 3": "ぺるそな3",
"persona 4": "ぺるそな4",
"persona 5": "ぺるそな5",
"pokemon": "ぽけもん",
"re:zero": "りぜろ",
"sailor moon": "せーらーむーん",
"serial experiments lain": "しりあるえくすぺりめんつれいん",
"shakugan no shana": "しゃくがんのしゃな",
"shingeki no kyojin": "しんげきのきょじん",
"skullgirls": "すかるがーるず",
"splatoon": "すぷらとぅーん",
"spy x family": "すぱいふぁみりー",
"ssss.gridman": "すっすっすっすぐりっどまん",
"street fighter": "すとりーとふぁいたー",
"super mario": "すーぱーまりお",
"to love-ru": "とらぶる",
"toaru kagaku no railgun": "とあるかがくのれいるがん",
"toaru majutsu no index": "とあるまじゅつのいんでっくす",
"tokyo ghoul": "とうきょうぐーる",
"touhou": "とうほう",
"uma musume": "うまむすめ",
"utau": "うたう",
"uzaki-chan": "うざきちゃん",
"violet evergarden": "ゔぁいおれっと",
"vocaloid": "ぼーかろいど",
"voiceroid": "ぼいすろいど",
"vspo!": "ぶいすぽ",
"vtuber": "ぶいちゅーばー",
"xenosaga": "ぜのさーが",
"yofukashi no uta": "よふかしのうた",
"yu-gi-oh!": "yu-gi-oh!",
"zenless zone zero": "ぜんれす"
};
/* ===== PRESET META ===== */
const PRESET_META = {
general: { label:'汎用', colorClass:'src-general', group:'general', priority:0 },
angle: { label:'アングル', colorClass:'src-general', group:'general', priority:3 },
rating: { label:'レーティング', colorClass:'src-rating', group:'rating', priority:2 },
year: { label:'年代', colorClass:'src-general', group:'general', priority:1 },
chara: { label:'キャラ', colorClass:'src-chara', group:'chara', priority:10 },
chara_attr: { label:'キャラ属性', colorClass:'src-chara', group:'chara', priority:11 },
appear_haircolor:{ label:'髪色', colorClass:'src-appear', group:'appear', priority:20 },
appear_hairstyle:{ label:'髪型', colorClass:'src-appear', group:'appear', priority:21 },
appear_eye: { label:'目', colorClass:'src-appear', group:'appear', priority:22 },
appear_face: { label:'顔・鼻・口', colorClass:'src-appear', group:'appear', priority:23 },
appear_skin: { label:'肌の色', colorClass:'src-appear', group:'appear', priority:24 },
appear_bodyshape: { label:'体形', colorClass:'src-appear', group:'appear', priority:25 },
appear_accessory: { label:'アクセサリー', colorClass:'src-appear', group:'appear', priority:26 },
appear_height: { label:'身長', colorClass:'src-appear', group:'appear', priority:27 },
appear_special: { label:'特殊外見', colorClass:'src-appear', group:'appear', priority:28 },
costume_casual: { label:'日常・カジュアル', colorClass:'src-costume', group:'costume', priority:29 },
costume_uniform: { label:'制服・礼装', colorClass:'src-costume', group:'costume', priority:30 },
costume_fantasy: { label:'ファンタジー', colorClass:'src-costume', group:'costume', priority:31 },
style: { label:'絵柄', colorClass:'src-style', group:'style', priority:35 },
animagine: { label:'ANI-POS', colorClass:'src-animagine', group:'ani', priority:-1 },
pony: { label:'PONY-POS', colorClass:'src-pony', group:'pony', priority:70 },
neg_animagine: { label:'ANI-NEG', colorClass:'src-neg_animagine', group:'neg', priority:80 },
neg_ani_skin: { label:'ANI-肌色', colorClass:'src-neg_animagine', group:'neg', priority:81 },
neg_pony: { label:'PONY-NEG', colorClass:'src-neg_pony', group:'neg', priority:90 },
nsfw_rating: { label:'NSFWレーティング', colorClass:'src-nsfw', group:'nsfw', priority:40 },
nsfw_body_f: { label:'女性の体', colorClass:'src-nsfw', group:'nsfw', priority:41 },
nsfw_body_m: { label:'男性の体', colorClass:'src-nsfw', group:'nsfw', priority:42 },
nsfw_position: { label:'体位', colorClass:'src-nsfw', group:'nsfw', priority:43 },
nsfw_vaginal: { label:'膣内', colorClass:'src-nsfw', group:'nsfw', priority:44 },
nsfw_oral: { label:'口淫', colorClass:'src-nsfw', group:'nsfw', priority:45 },
nsfw_anal: { label:'アナル', colorClass:'src-nsfw', group:'nsfw', priority:46 },
nsfw_handjob: { label:'手コキ', colorClass:'src-nsfw', group:'nsfw', priority:45 },
nsfw_paizuri: { label:'パイズリ', colorClass:'src-nsfw', group:'nsfw', priority:46 },
nsfw_cum: { label:'射精', colorClass:'src-nsfw', group:'nsfw', priority:47 },
nsfw_squirt: { label:'潮吹き', colorClass:'src-nsfw', group:'nsfw', priority:47 },
nsfw_reaction: { label:'反応・顔', colorClass:'src-nsfw', group:'nsfw', priority:48 },
nsfw_costume: { label:'衣装', colorClass:'src-nsfw', group:'nsfw', priority:49 },
nsfw_bondage: { label:'拘束', colorClass:'src-nsfw', group:'nsfw', priority:50 },
nsfw_yuri: { label:'百合', colorClass:'src-nsfw', group:'nsfw', priority:51 },
nsfw_futa: { label:'フタナリ', colorClass:'src-nsfw', group:'nsfw', priority:52 },
nsfw_monster: { label:'触手/怪物', colorClass:'src-nsfw', group:'nsfw', priority:53 },
nsfw_scenario: { label:'シナリオ', colorClass:'src-nsfw', group:'nsfw', priority:54 },
nsfw_fetish: { label:'フェチ', colorClass:'src-nsfw', group:'nsfw', priority:55 },
nsfw_pose: { label:'エロポーズ', colorClass:'src-nsfw', group:'nsfw', priority:56 },
nsfw_misc: { label:'その他', colorClass:'src-nsfw', group:'nsfw', priority:57 },
// 実写専用
photo_quality: { label:'写真品質', colorClass:'src-general', group:'photo', priority:60 },
photo_neg: { label:'写真ネガ', colorClass:'src-neg-ani', group:'photo', priority:61 },
photo_subject: { label:'被写体', colorClass:'src-general', group:'photo', priority:62 },
photo_location:{ label:'ロケーション', colorClass:'src-general', group:'photo', priority:63 },
photo_uniform: { label:'制服・学校', colorClass:'src-costume', group:'photo', priority:64 },
photo_lighting:{ label:'照明', colorClass:'src-general', group:'photo', priority:65 },
};
/* ===== NSFW TAB KEYS (for all-random) ===== */
const NSFW_TABS = ['nsfw_rating','nsfw_body_f','nsfw_body_m','nsfw_position','nsfw_vaginal','nsfw_oral','nsfw_anal','nsfw_handjob','nsfw_paizuri','nsfw_cum','nsfw_squirt','nsfw_reaction','nsfw_costume','nsfw_bondage','nsfw_yuri','nsfw_futa','nsfw_monster','nsfw_scenario','nsfw_fetish','nsfw_pose','nsfw_misc'];
/* ===== TAG JAPANESE LABELS ===== */
const TAG_JP = {
// === 汎用・品質 ===
'pure white background':'何もない純白の背景・キャラが際立つ',
'simple background':'シンプルな無地背景・背景を単色に抑える',
'white background':'白い背景・白単色',
// === angle タグ日本語訳 ===
// カメラ方向
'from above':'上から見下ろしたアングル',
'from below':'下から見上げたアングル',
'from behind':'後ろからのアングル・背面視点',
'from side':'横からのアングル・サイドビュー',
'from front':'正面からのアングル',
'bird\'s-eye view':'真上からの俯瞰アングル',
'worm\'s-eye view':'地面から見上げた虫の目アングル',
'aerial view':'空中から見た俯瞰アングル',
'overhead view':'真上・真俯瞰アングル',
'top-down view':'真上からの見下ろしアングル',
'low angle':'低い位置からの見上げアングル',
'high angle':'高い位置からの見下ろしアングル',
'eye level':'目線の高さの水平アングル',
'dutch angle':'斜めに傾けたカメラアングル・緊張感演出',
'tilted angle':'カメラが傾いたアングル',
'three-quarter view':'斜め45度の3/4アングル',
'isometric view':'等角投影・斜め上からの俯瞰',
'diagonal angle':'対角線を使った斜めアングル',
// ショットサイズ
'extreme close-up':'顔や体の一部を超アップで切り取る',
'close-up':'顔・上半身をアップで写す',
'medium close-up':'胸〜顔が入るミディアムクローズアップ',
'medium shot':'腰〜頭が入る中距離ショット',
'long shot':'全身+周囲が入る遠景ショット',
'wide shot':'広い範囲を写すワイドショット',
'full shot':'全身が収まる全身ショット',
// フレーミング
'full body':'全身を写す・頭から足先まで',
'upper body':'上半身のみを写す',
'lower body':'下半身のみを写す',
'half body':'上半身・腰上を写すハーフボディ',
'bust shot':'胸から上を写すバストショット',
'waist up':'腰から上を写す',
'cowboy shot':'腰〜太ももあたりで切るカウボーイショット',
'head shot':'顔・頭部のみのショット',
'face focus':'顔にフォーカスしたショット',
'chest up':'胸上・胸から上を写す',
'knees up':'膝から上を写す',
'thighs up':'太ももから上を写す',
'portrait':'縦長・人物を縦構図で写す',
// POV・主観
'pov':'一人称視点・主観視点',
'first person view':'一人称視点・キャラの目線で見た視点',
'point of view':'視点・主観視点全般',
'selfie angle':'自撮りアングル・スマホを持ち上げた視点',
'over the shoulder shot':'肩越しショット・後ろから肩越しに見る',
'vanishing point':'消失点を使ったパース構図',
// 視線・向き
'looking back':'振り返る・後ろを振り向く',
'looking up':'上を見上げる',
'looking down':'下を見下ろす',
'looking away':'視線を外している・目を逸らす',
'looking to the side':'横を向いている・横目',
'looking over shoulder':'肩越しに振り返る',
'eye contact':'目が合っている・アイコンタクト',
'direct gaze':'まっすぐカメラを見つめる',
'turning around':'振り返っている動作中',
'facing away':'こちらに背を向けている',
'facing viewer':'視聴者の方を向いている',
'back to viewer':'背中を向けている',
// レンズ・光学
'fisheye lens':'魚眼レンズ・極端に歪んだ広角',
'wide angle lens':'広角レンズ・広い範囲を写す',
'telephoto lens':'望遠レンズ・遠くを圧縮して写す',
'macro shot':'マクロ撮影・細部を拡大して写す',
'depth of field':'被写界深度・前後をぼかして主題を際立てる',
'bokeh':'ボケ・背景が玉ボケになる',
'shallow focus':'浅い焦点・ピントが合う範囲が狭い',
'motion blur':'動きによるブレ・スピード感の演出',
'lens flare':'レンズフレア・光の反射・逆光',
// 構図・配置
'centered':'画面中央にキャラを配置',
'off-center':'中央からずらしたオフセンター構図',
'symmetrical':'左右対称の構図',
'negative space':'余白を活かした構図・空間を広くとる',
'dramatic angle':'ドラマチックなアングル・迫力ある構図',
'dynamic angle':'動きのある躍動感のあるアングル',
'action angle':'アクション・動きを強調するアングル',
'profile view':'真横からのプロフィールビュー',
'rear view':'真後ろからのリアビュー',
'side view':'横からの側面ビュー',
'front view':'真正面からのフロントビュー',
'solo focus':'1人に焦点を当てた構図',
'character focus':'キャラクターにフォーカスした構図',
'head out of frame':'頭が画面外にはみ出したフレーミング・首〜体のみ写る',
// === chara_attr タグ日本語訳 ===
// デレ系
'tsundere':'ツンデレ・最初はツンツン後にデレる',
'yandere':'ヤンデレ・愛が歪んだ危険な執着',
'kuudere':'クーデレ・クールで無表情だが内に愛がある',
'dandere':'ダンデレ・無口で内気だが心を開くと甘い',
'deredere':'デレデレ・常にラブラブ甘々な子',
'himedere':'ヒメデレ・お姫様気質・高飛車な甘えん坊',
'kamidere':'カミデレ・神様気質・自分を崇拝させたがる',
'sadodere':'サドデレ・サド気質だが内心デレている',
'masodere':'マゾデレ・マゾ気質・痛みに快感を覚える',
'dorodere':'ドロドロデレ・表面は普通だが内面が病んでいる',
'bakadere':'バカデレ・天然でお馬鹿な甘えん坊',
'hajidere':'ハジデレ・好きな人の前で極度に恥ずかしがる',
'hinedere':'ヒネデレ・ひねくれ者だが愛はある',
'utsudere':'ウツデレ・うつっぽい・暗いが愛に救われる',
'goudere':'ゴウデレ・強引に愛を押しつけてくるタイプ',
// 明るい・ポジティブ系
'cheerful':'明るく元気いっぱいな性格',
'energetic':'エネルギッシュ・活動的',
'optimistic':'楽観的・ポジティブ思考',
'bubbly':'はじけるような明るさ・わいわい系',
'lively':'生き生きとして活発',
'playful':'遊び好き・おちゃめ',
'mischievous':'いたずら好き・悪戯っ子',
'carefree':'のんびり屋・何も気にしない',
'easygoing':'おおらか・マイペース',
'friendly':'人懐っこい・フレンドリー',
'kind':'優しい・親切',
'gentle':'穏やか・ソフトな性格',
'warm':'温かみのある・ほっこり系',
'caring':'面倒見がよい・お世話好き',
'affectionate':'愛情深い・スキンシップ好き',
'innocent':'無邪気・汚れを知らない',
'pure':'純粋・真っ直ぐな心',
'naive':'世間知らず・うぶ',
'childlike':'子供っぽい・無邪気',
'sincere':'誠実・嘘をつかない',
// クール・ミステリアス系
'stoic':'感情を表に出さない・無口',
'cold':'冷淡・人を寄せ付けない',
'aloof':'超然としている・距離感がある',
'distant':'心理的に遠い・馴れ馴れしくない',
'mysterious':'謎めいている・素性が不明',
'enigmatic':'謎に包まれた存在',
'dark':'ダーク・影のある性格',
'brooding':'物思いにふける・内省的',
'gloomy':'暗い・どんよりした雰囲気',
'melancholic':'メランコリック・哀愁漂う',
'lonely':'孤独・一人でいることが多い',
'quiet':'物静か・あまり喋らない',
'reserved':'控えめ・自己主張しない',
// 強気・自信系
'confident':'自信に満ちている',
'arrogant':'傲慢・自分が一番だと思っている',
'prideful':'プライドが高い',
'dominant':'支配的・場を仕切るタイプ',
'assertive':'はっきり自己主張する',
'bold':'大胆・物怖じしない',
'daring':'冒険好き・無謀にも挑戦する',
'fearless':'恐れ知らず',
'brave':'勇敢・勇気がある',
'courageous':'勇気ある行動をとる',
'charismatic':'カリスマ性がある・人を引きつける',
'leader type':'リーダータイプ・まとめ役',
'ambitious':'野心家・高い目標を持つ',
// 内向き・弱気系
'timid':'気が弱い・おどおどしている',
'nervous':'緊張しやすい・そわそわ',
'anxious':'不安がち・心配性',
'submissive':'従順・言われたことに従う',
'clumsy':'ドジ・よく失敗する',
'airheaded':'天然・ぼんやりしている',
'ditzy':'おっちょこちょい・うっかり屋',
'awkward':'不器用・空気が読めない',
'insecure':'自信がない・自己否定しがち',
'sensitive':'傷つきやすい・感受性が強い',
'emotional':'感情豊か・泣きやすい',
'crybaby':'泣き虫',
// 特殊感情・執着系
'obsessive':'執着心が強い・こだわりが激しい',
'possessive':'独占欲が強い・独り占めしたがる',
'jealous':'嫉妬深い',
'yandere-like':'ヤンデレっぽい・愛が重め',
'protective':'守護的・大切な人を守ろうとする',
'devoted':'一途・一人に全力を注ぐ',
'loyal':'忠実・裏切らない',
'clingy':'べったり・離れたがらない',
'sadistic':'サディスティック・意地悪を楽しむ',
'masochistic':'マゾヒスティック・痛みや苦痛を好む',
'manipulative':'人を操ることが得意',
'deceptive':'欺瞞的・騙すのが上手い',
// 知的・個性系
'genius':'天才・突出した知性',
'intellectual':'知的・学問好き',
'bookworm':'本の虫・読書家',
'nerdy':'理系オタク気質',
'otaku':'オタク・好きなものへの熱量が高い',
'mature':'大人びた・精神的に成熟',
'wise':'賢い・物事の本質を見抜く',
'calm':'冷静・落ち着いている',
'rational':'合理的・感情より論理',
'logical':'論理的思考',
'creative':'創造力がある',
'artistic':'芸術家気質',
'musical':'音楽的センスがある',
// 活発・アクション系
'tomboyish':'ボーイッシュ・男の子っぽい',
'athletic':'スポーツ万能・運動神経がいい',
'sporty':'スポーツ好き',
'competitive':'負けず嫌い・競争心が強い',
'reckless':'無謀・後先を考えない',
'impulsive':'衝動的・考える前に動く',
'hot-headed':'短気・すぐカッとなる',
'stubborn':'頑固・意見を曲げない',
'rebellious':'反抗的・規則に従わない',
'delinquent':'不良・ヤンキー気質',
'rough':'荒っぽい・雑な言動',
'wild':'野性的・自由奔放',
// 女の子らしい・華やか系
'girly':'女の子らしい・フェミニン',
'elegant':'上品・エレガント',
'graceful':'優雅な立ち居振る舞い',
'refined':'洗練された・品がある',
'ladylike':'淑女らしい・礼儀正しい',
'ojou-sama':'お嬢様・高貴な生まれ育ち',
'princess-like':'お姫様っぽい・大切に育てられた感',
'flirtatious':'いちゃいちゃする・色っぽい言動',
'charming':'魅力的・人を惹きつける',
'seductive':'セダクティブ・誘惑的',
// ロール・立場属性
'onee-san type':'お姉さんタイプ・面倒見のいい年上感',
'imouto type':'妹タイプ・甘えてくる年下感',
'senpai':'先輩・上の立場でリードする',
'kouhai':'後輩・慕ってくる立場',
'childhood friend':'幼馴染・ずっと側にいた存在',
'transfer student':'転校生・新参者・ミステリアスな新入り',
'honor student':'優等生・成績優秀・模範的',
'idol':'アイドル・みんなの憧れの存在',
'gyaru':'ギャル・派手でノリのいい女の子',
'kunoichi type':'くノ一タイプ・忍者系女子',
// 特殊設定
'amnesiac':'記憶喪失・自分の過去を覚えていない',
'time traveler':'時間旅行者・別の時代からきた存在',
'reincarnated':'転生者・別世界・別生から転生',
'half-human':'半人半妖・人間と別の種族の混血',
'last of her kind':'同族最後の生き残り',
'chosen one':'選ばれた者・特別な運命を持つ',
'cursed':'呪われた存在・呪いを背負っている',
'haunted':'霊に憑かれている・心霊体質',
'possessed':'何かに取り憑かれた状態',
// === 目タグ追加 ===
'beautiful eyes':'美しく描かれた目・魅力的な瞳',
'detailed eyes':'虹彩・ハイライトまで細かく描き込まれた目',
'distinct pupils':'くっきりと見える瞳孔・明確な黒目',
'no sclera':'白目がない・目全体が色で塗られている',
'colored sclera':'白目部分に色がついている(黒目や赤目など)',
'solid eyes':'目全体が単色で塗りつぶされた状態',
'background':'背景全般・弾くと背景描写を抑制',
'scenery':'風景・景色の描写・弾くと背景を消す',
'building':'建物・建築物の描写・弾くと街並みを消す',
'nature':'自然(木・草・山等)の描写・弾くと自然背景を消す',
'detailed background':'細かく描き込まれた背景・弾くとシンプル背景になる',
'1girl':'女の子1人を主役に',
'2girls':'女の子2人の構図',
'1boy':'男の子1人を主役に',
'1other':'性別を明示しないキャラ1人',
'harem':'複数人に囲まれたハーレム構図',
'looking at viewer':'カメラ目線でこちらを見ている',
'full body':'頭からつま先まで全身が入る構図',
'masterpiece':'傑作・全体的に最高品質な絵',
'best quality':'最高品質',
'very aesthetic':'美的センス高め・絵として映える',
'absurdres':'解像度が極めて高く細部まで鮮明',
'newest':'最新の学習データ寄り',
'highres':'高解像度',
'ultra-detailed':'あらゆる部位を超緻密に描写',
'sharp focus':'輪郭・細部がくっきりシャープ',
'high resolution':'高解像度highresと同義',
'incredibly absurdres':'absurdresの強化版・超超高解像度',
'high score':'品質スコアが高い絵',
'great score':'品質スコア優秀な絵',
'ultra high res':'超高解像度',
'8k':'8K解像度相当の精緻さ',
'4k':'4K解像度相当の精緻さ',
'highly detailed':'細部まで丁寧に描き込まれた',
'professional':'プロが描いたような品質感',
'beautiful lighting':'光源が美しく描かれている',
'cinematic':'映画のカット割りのような臨場感',
'perfect anatomy':'体の比率・構造が正確',
'detailed background':'背景も細かく描き込まれた',
'official art':'公式イラスト・スタジオクオリティの仕上がり',
'clean lineart':'綺麗な線画・乱れのない輪郭線',
'cel shading':'アニメ塗り・セル画風の塗り分け',
'crisp detail':'鮮明なディテール・細部まではっきりした表現',
// === アングル・構図 ===
'portrait':'顔〜胸上のポートレートショット',
'close-up':'顔や一部を大写しにしたクローズアップ',
'medium shot':'腰から上が入るミドルショット',
'wide shot':'シーン全体が入るワイドショット',
'fisheye':'広角歪みの魚眼レンズ効果',
'cowboy shot':'太もも〜胸上が入るカウボーイショット',
'upper body':'胸から上の上半身ショット',
'lower body':'腰から下の下半身ショット',
'waist up':'腰から上が見えるショット',
'knees up':'膝より上が見えるショット',
'from above':'高い位置から見下ろした俯瞰アングル',
'from below':'下から見上げたあおりアングル',
'from side':'真横から見た横顔・側面構図',
'from behind':'後ろ姿・後方視点',
'dutch angle':'カメラを傾けたダッチアングル',
'dynamic angle':'迫力ある動的アングル',
'action pose':'躍動感のあるアクションポーズ',
'standing':'自然な立ち姿',
'sitting':'腰を下ろした座りポーズ',
'lying':'床や地面に横たわる',
'kneeling':'膝をついた跪きポーズ',
'squatting':'膝を曲げてしゃがんだポーズ',
'leaning forward':'上体を前に傾けた前傾みポーズ',
'leaning back':'上体を後方に反らせたポーズ',
'arms up':'両腕を頭上に上げたポーズ',
'arms behind head':'手を頭の後ろで組むポーズ',
'arms behind back':'腕を背中の後ろに回したポーズ',
'hand on hip':'片手を腰に当てて立つポーズ',
'crossed arms':'胸の前で腕を組むポーズ',
'on all fours':'手足をついた四つん這いポーズ',
'spread arms':'左右に両腕を広げたポーズ',
'reaching out':'カメラに向かって手を伸ばすポーズ',
'solo':'キャラ1人のみの構図',
'solo focus':'1人にフォーカスした構図',
'multiple girls':'女性が複数いる構図',
'multiple boys':'男性が複数いる構図',
'1boy':'男の子1人を主役に',
'2boys':'男の子2人の構図',
'2girls':'女の子2人の構図',
'girl and boy':'男女ペアの構図',
'couple':'カップル・2人組',
'group':'複数人のグループ構図',
'looking away':'視線が画面外・よそ見している',
'looking back':'後ろを振り返って目線を向ける',
'looking up':'上目遣いで見上げている',
'looking down':'目線を落として下を見ている',
// === 表情 ===
'smile':'自然で柔らかい笑顔',
'grin':'ニヤリとした得意げな笑顔',
'smirk':'口角だけ上げた不敵な笑み',
'blush':'恥ずかしさや興奮で頬が赤い',
'shy':'照れや恥じらいを感じる表情',
'serious':'目を細めた真剣・無表情に近い顔',
'expressionless':'感情を出さない無表情',
'surprised':'目を見開いて驚いた表情',
'happy':'目を細めて嬉しそうな明るい表情',
'sad':'眉を下げた悲しそうな表情',
'angry':'眉間にしわを寄せた怒り顔',
'embarrassed':'目を逸らして赤面した照れ顔',
'seductive':'半目で見つめる色気のある誘惑的な表情',
'sleepy':'とろんとした眠そうな目',
'crying':'涙を流して泣いている表情',
'pout':'口を尖らせたふくれっ面',
// === 絵柄スタジオ ===
'kyoto animation':'京アニ風・水彩的で柔らかく美しい色彩と作画',
'production i.g':'プロダクションI.G風・精緻でシャープなメカや都市描写',
'ufotable':'ufotable風・鮮やかで立体的なエフェクトと滑らかな作画',
'wit studio':'WIT STUDIO風・繊細で力強い線と臨場感のある構図',
'cloverworks':'CloverWorks風・丁寧な塗りとキャラの感情表現',
'a-1 pictures':'A-1 Pictures風・安定した綺麗な作画と鮮やかな色彩',
'trigger':'TRIGGER風・爆発的なエネルギーと大胆な線と色',
'mappa':'MAPPA風・力強い線と高精細なキャラ描写',
'shaft':'シャフト風・独特のレイアウトと芸術的な演出感',
'toei animation':'東映アニメーション風・太めの輪郭線とポップな色彩',
'studio ghibli':'ジブリ風・暖かみのある背景と自然豊かな色彩',
// === アニメスタイル ===
'intricate details':'装飾・背景・服など細部まで書き込む',
'detailed face':'顔の造形を細かく描写',
'beautiful detailed eyes':'虹彩・反射光まで繊細に描いた目',
'beautiful detailed face':'顔全体を美しく緻密に描写',
'expressive eyes':'感情が伝わる豊かな目の描写',
'skin pores':'毛穴まで見える超リアルな肌質感',
'realistic texture':'布・金属・皮膚など素材感がリアル',
'highly detailed':'細部まで丁寧に描き込まれた',
// === 照明 ===
'cinematic lighting':'映画のような劇的な照明効果',
'soft lighting':'影が柔らかく肌が優しく見える光',
'dramatic lighting':'明暗の強いコントラストで迫力を出す照明',
'rim lighting':'輪郭を光で縁取るリムライト',
'backlighting':'逆光でシルエットが映えるバックライト',
'natural light':'太陽光や窓光の自然な光源',
'golden hour':'夕暮れのオレンジ・金色の温かい光',
'neon lights':'都市的なネオンサインの色とりどりの光',
'volumetric light':'空気中の粒子で光の筋が見えるゴッドレイ',
'lens flare':'レンズ反射の光のにじみ',
'bloom':'光が滲んで柔らかく輝くブルーム効果',
// === ANI/PONY スコア ===
'score_9':'最高スコア9・最高品質の絵',
'score_8_up':'スコア8以上・高品質',
'score_7_up':'スコア7以上・良質',
'score_6_up':'スコア6以上・標準以上',
'score_5_up':'スコア5以上・平均以上',
'score_4_up':'スコア4以上・最低限の品質',
'source_anime':'アニメ風の絵柄を優先',
'source_furry':'ファーリー(獣人)風の絵柄',
'source_cartoon':'カートゥーン・アメコミ風',
'source_pony':'マイリトルポニー風絵柄',
'rating_safe':'全年齢・性的描写なし',
'rating_questionable':'際どいが直接的な性描写なし',
'rating_explicit':'明確な性描写あり・成人向け',
'detailed':'細部まで描き込まれた',
'high quality':'高品質',
// === レーティング・年代 ===
'safe':'性的描写なし・全年齢向け',
'sensitive':'水着・際どい衣装など軽度のセンシティブ',
'explicit':'明確な性描写あり・成人向け',
'nsfw':'成人向けコンテンツ',
'year 2005':'西暦2005年頃の絵柄・作画スタイル',
'year 2006':'西暦2006年頃の絵柄・作画スタイル',
'year 2007':'西暦2007年頃の絵柄・作画スタイル',
'year 2008':'西暦2008年頃の絵柄・作画スタイル',
'year 2009':'西暦2009年頃の絵柄・作画スタイル',
'year 2010':'西暦2010年頃の絵柄・作画スタイル',
'year 2011':'西暦2011年頃の絵柄・作画スタイル',
'year 2012':'西暦2012年頃の絵柄・作画スタイル',
'year 2013':'西暦2013年頃の絵柄・作画スタイル',
'year 2014':'西暦2014年頃の絵柄・作画スタイル',
'year 2015':'西暦2015年頃の絵柄・作画スタイル',
'year 2016':'西暦2016年頃の絵柄・作画スタイル',
'year 2017':'西暦2017年頃の絵柄・作画スタイル',
'year 2018':'西暦2018年頃の絵柄・作画スタイル',
'year 2019':'西暦2019年頃の絵柄・作画スタイル',
'year 2020':'西暦2020年頃の絵柄・作画スタイル',
'year 2021':'西暦2021年頃の絵柄・作画スタイル',
'year 2022':'西暦2022年頃の絵柄・作画スタイル',
'year 2023':'西暦2023年頃の絵柄・作画スタイル',
'year 2024':'西暦2024年頃の絵柄・作画スタイル',
'year 2025':'西暦2025年頃の絵柄・作画スタイル',
// === NEG タグ ===
'worst quality':'最低品質・生成を弾く',
'low quality':'低品質・生成を弾く',
'normal quality':'普通品質・弾いて高品質を狙う',
'lowres':'低解像度・弾く',
'jpeg artifacts':'JPEG圧縮のブロックイズ・弾く',
'compression artifacts':'圧縮劣化・弾く',
'blurry':'全体がぼやけている・弾く',
'blur':'ぼかし・弾く',
'noise':'ノイズが目立つ・弾く',
'pixelated':'ドット化・ピクセル崩れ・弾く',
'bad anatomy':'体の構造が崩れている・弾く',
'bad hands':'手の描写が崩れている・弾く',
'bad feet':'足の描写が崩れている・弾く',
'missing fingers':'指が足りない・弾く',
'extra digit':'余分な指がある・弾く',
'fewer digits':'指の本数が少ない・弾く',
'extra limbs':'余分な手足・弾く',
'missing limbs':'手足が欠けている・弾く',
'malformed limbs':'手足が歪んでいる・弾く',
'fused fingers':'指がくっついている・弾く',
'too many fingers':'指が多すぎる・弾く',
'mutated hands':'手が変異している・弾く',
'cloned face':'顔が複製されたような崩れ・弾く',
'deformed':'体が変形している・弾く',
'disfigured':'容貌が醜く崩れている・弾く',
'ugly':'醜い仕上がり・弾く',
'mutilated':'切断・破損した描写・弾く',
'gross proportions':'体の比率が異常・弾く',
'bad proportions':'全体の比率がおかしい・弾く',
'long neck':'首が異常に長い・弾く',
'extra arms':'余分な腕・弾く',
'extra legs':'余分な脚・弾く',
'mutation':'変異した体・弾く',
'text':'テキストや文字が入る・弾く',
'error':'エラー描写・弾く',
'signature':'サイン・署名・弾く',
'watermark':'透かしウォーターマーク・弾く',
'username':'ユーザー名テキスト・弾く',
'artist name':'作者名テキスト・弾く',
'logo':'ロゴマーク・弾く',
'stamp':'スタンプ・弾く',
'patreon logo':'Patreonロゴ・弾く',
'cropped':'画像が途中で切れている・弾く',
'out of frame':'キャラがフレーム外に出る・弾く',
'cut off':'体が切断されている・弾く',
'partial body':'体の一部しか描かれていない・弾く',
'sketch':'スケッチ・下書き風の仕上がり・弾く',
'doodle':'落書き風の仕上がり・弾く',
'rough draft':'ラフ画・下書き段階・弾く',
'messy lines':'雑な線・乱れた線画・弾く',
'rough lines':'荒い線・粗い線画・弾く',
'traditional media':'アナログ画材(絵の具・鉛筆等)・弾く',
'watercolor':'水彩画風・弾く',
'pencil':'鉛筆画風・弾く',
'rough sketch':'ラフスケッチ・弾く',
'draft':'下書き状態・弾く',
'unfinished':'未完成の仕上がり・弾く',
'flat color':'ベタ塗り・陰影なし',
'flat shading':'フラットなシェーディング・弾く',
'duplicate':'同じ絵が重複・弾く',
'morbid':'病的・グロテスクな描写・弾く',
'poorly drawn face':'顔の描写が雑・弾く',
'poorly drawn hands':'手の描写が雑・弾く',
'monochrome':'モノクロ・弾く',
'out of focus':'焦点が外れてぼやけている・弾く',
'score_1':'スコア1・最低品質・弾く',
'score_2':'スコア2・低品質・弾く',
'score_3':'スコア3・低品質・弾く',
'score_4':'スコア4・低品質・弾く',
'noisy':'ノイズが多い・弾く',
'fused body parts':'体の部位がくっついている・弾く',
'malformed':'変形した体・弾く',
'source_real':'実写ソース・アニメ絵に弾く',
'3d render':'3DCGレンダリング・弾く',
'realistic':'リアル系・弾く',
'photo':'写真・弾く',
'photograph':'写真・弾く',
'grayscale':'グレースケール・弾く',
'poorly drawn':'全体的に雑な描写・弾く',
'low score':'低スコア・弾く',
'bad score':'悪いスコア・弾く',
'average score':'平均スコア・弾く',
'extra digits':'余分な数字や指・弾く',
// === NSFW レーティング ===
'uncensored':'修正なし・無修正',
'no censor':'修正なしuncensoredと同義',
'mosaic censorship':'モザイク修正あり',
'bar censor':'棒状の黒線修正あり',
'completely nude':'一切の衣類なし・完全全裸',
'nude':'完全全裸',
'nude filter':'服が透過した裸フィルター',
'rating_explicit':'明確な性描写あり・成人向け',
'rating_questionable':'際どいが直接的な性描写なし',
'partially censored':'一部にだけ修正がある',
// === 女性ボディ ===
'breasts':'胸(描写あり)',
'small breasts':'小ぶりな胸',
'medium breasts':'標準的な胸の大きさ',
'large breasts':'大きめの胸',
'huge breasts':'かなり大きい巨乳',
'gigantic breasts':'非現実的なほど超巨大な胸',
'flat chest':'ほぼ平らな胸・貧乳',
'perky breasts':'上向きでハリのある胸',
'sagging breasts':'垂れた胸',
'bouncing breasts':'動作に合わせて揺れる胸',
'breast press':'胸を何かに押しつけている',
'cleavage':'胸の谷間が見えている',
'deep cleavage':'深い胸の谷間',
'underboob':'胸の下が見えている',
'sideboob':'胸の横が見えている',
'topless':'上半身裸・胸が露出',
'bare breasts':'胸が露わになっている',
'nipples':'乳首が描写されている',
'erect nipples':'勃起した乳首',
'inverted nipples':'陥没乳首',
'puffy nipples':'ふくらんだぷっくり乳首',
'dark nipples':'色の濃い乳首',
'pink nipples':'ピンク色の乳首',
'large areolae':'広い乳輪',
'areolae':'乳輪の描写',
'nipple piercing':'乳首ピアス',
'pussy':'女性器の描写',
'wet pussy':'濡れた状態の女性器',
'dripping pussy':'愛液が滴る女性器',
'spread pussy':'開いた状態の女性器',
'puffy pussy':'ぷっくりとした女性器',
'hairy pussy':'毛があるまんこ',
'shaved pussy':'剃毛された女性器',
'visible pussy':'女性器が見えている',
'pussy juice':'愛液・女性器からの分泌液',
'labia':'陰唇の描写',
'labia minora':'小陰唇',
'labia majora':'大陰唇',
'clitoris':'クリトリスの描写',
'clitoral hood':'陰核包皮',
'vaginal opening':'膣口の描写',
'cervix':'子宮口の描写',
'uterus':'子宮の描写(断面図系)',
'slim body':'細くスリムな体型',
'thick thighs':'太くセクシーな太もも',
'wide hips':'広い腰幅',
'big hips':'大きなヒップ',
'curvy':'くびれのある曲線美な体型',
'plump':'ぽっちゃりとした丸みのある体型',
'flat stomach':'引き締まった平らなお腹',
'abs':'腹筋が割れている',
'toned body':'引き締まった筋肉質な体',
'soft body':'柔らかそうなたぷたぷな体',
'chubby':'ふくよかな体型',
'ass':'お尻の描写',
'big ass':'大きなお尻',
'round ass':'丸くてきれいなお尻',
'bubble butt':'丸くプリっとしたお尻',
'ass grab':'お尻をつかんでいる',
'butt crack':'臀裂・お尻の割れ目が見える',
'navel':'へそが見えている',
'belly button':'へそnavelと同義',
'armpits':'腋が見えている',
'exposed armpits':'露出した腋',
'inner thigh':'内太ももが見える',
// === 男性ボディ ===
'penis':'男性器の描写',
'erect penis':'勃起した男性器',
'flaccid penis':'萎えた状態の男性器',
'huge cock':'巨大な男性器',
'thick cock':'太い男性器',
'long cock':'長い男性器',
'small penis':'小さな男性器',
'veiny penis':'血管が浮き出た男性器',
'throbbing cock':'脈打つように硬い男性器',
'foreskin':'包皮が残っている',
'uncircumcised':'包茎・未割礼',
'balls':'睾丸の描写',
'testicles':'睾丸ballsと同義',
'scrotum':'陰嚢',
'taint':'会陰・股間の間',
'pubic hair':'陰毛の描写',
'male pubic hair':'男性の陰毛',
'masculine body':'男らしい筋肉質な体型',
'muscular':'筋肉が発達した体',
'chest hair':'胸毛',
// === NSFW 体位 ===
'missionary':'正常位・正面から向き合う基本体位',
'doggy style':'後背位・四つん這いバック',
'cowgirl position':'女性が上になる騎乗位',
'reverse cowgirl':'後ろ向き騎乗位',
'mating press':'足を抱えて深く挿入する体位',
'standing sex':'立ったまま行う体位',
'wall sex':'壁に押しつけての立位',
'pile driver':'足を肩にかけ垂直に挿入',
'lotus position':'正面で向き合い脚を絡める蓮華座体位',
'spoon position':'互いに横向きで寄り添うスプーン体位',
'prone bone':'うつ伏せのまま後ろからの体位',
'face down ass up':'顔を下げ腰を持ち上げたポーズ',
'frog position':'カエルのように脚を開いた体位',
'amazon position':'女性が上に乗り前後を逆にした体位',
'wheelbarrow position':'相手が後ろから足を持つ体位',
'suspended congress':'宙吊りで向き合う体位',
'from behind standing':'立ったまま後ろから挿入',
'lifted and penetrated':'持ち上げられながら挿入される',
'carrying sex':'おんぶ・抱っこしながらのセックス',
'against glass':'ガラスに押しつけながらのセックス',
'legs up':'両足を高く上げた体位',
'spread legs':'脚を大きく広げた状態',
'legs over shoulders':'脚を相手の肩にかけた体位',
'legs behind head':'足が頭の後ろまで上がった超柔軟体位',
'leg lock':'足で相手の腰を引き寄せる体位',
'riding':'騎乗して乗る',
'straddling':'またがって乗るポーズ',
'sitting on lap':'相手の膝の上に乗るポーズ',
'grinding':'密着してすりつけるグラインド',
'dry humping':'服のままこすりつけるドライハンプ',
'pressed against wall':'壁に押しつけられた状態',
'bent over':'前に上体を屈めたポーズ',
'bent over desk':'机に上体を倒した体位',
'bent over table':'テーブルに上体を倒した体位',
'thigh job':'太ももで挟む太もも性交',
'paizuri position':'胸で挟むパイズリ体位',
'lap pillow':'膝枕',
'missionary pov':'正常位の一人称・見下ろしアングル',
'pov sex':'セックスの一人称・主観視点',
'from above pov':'上から見下ろした主観視点',
'reverse sitting':'後ろ向きに乗った体位',
'face sitting position':'顔に乗る・フェイスシッティング体位',
'lying on back':'仰向けに寝ている',
'lying on stomach':'うつ伏せに寝ている',
'lying on side':'横向きに寝ている',
'on all fours sex':'四つん這いでの体位',
'floor sex':'床の上でのセックス',
'bed sex':'ベッドの上でのセックス',
'spread to bed':'ベッドに大の字で広げられた状態',
'tied to bed sex':'ベッドに縛り付けられた状態でのセックス',
'leaning forward sex':'前傾みで寄りかかった体位',
'against desk':'机に寄りかかった体位',
'legs apart':'脚を左右に開いたポーズ',
'spread eagle':'大の字に手足を広げた体勢',
// === 膣・挿入 ===
'vaginal':'膣内性交',
'vaginal sex':'膣内セックス',
'vaginal penetration':'膣内への挿入',
'deep penetration':'深く奥まで挿入している',
'shallow penetration':'浅くだけ挿入している',
'creampie':'膣内中出し',
'vaginal creampie':'膣内に中出し',
'cum inside':'体内に中出し',
'womb penetration':'子宮内への挿入描写',
'cervix penetration':'子宮口への挿入描写',
'fingering':'指を使った挿入・指マン',
'finger insertion':'指での挿入',
'two fingers':'2本の指での挿入',
'three fingers':'3本の指での挿入',
'knuckle deep':'根本まで深く指を挿入',
'fisting':'拳を挿入するフィスティング',
'fist insertion':'拳での挿入',
'sex toy':'オナホ・おもちゃを使用',
'dildo':'ディルドを使用',
'vibrator':'電動バイブを使用',
'wand vibrator':'ワンド型バイブを使用',
'butt plug in pussy':'バットプラグを膣に挿入',
'rubbing':'性器をこすりつける',
'masturbation':'自慰・オナニー',
'fingering herself':'自分の指で自慰',
'legs spread masturbation':'脚を開いて自慰',
'mutual masturbation':'2人で互いに手を使う',
'insertion':'挿入描写',
'object insertion':'物を挿入している',
// === 口淫 ===
'oral':'口を使った性行為・口淫',
'fellatio':'ペニスを舐める・フェラチオ',
'blowjob':'フェラfellatio と同義)',
'cunnilingus':'女性器を舐める・クンニ',
'deepthroat':'奥まで咥えるディープスロート',
'irrumatio':'動かず挿れるだけのイラマチオ',
'facefuck':'顔を固定して激しいイラマチオ',
'69':'互いに舐め合う69体位',
'face sitting':'顔に乗って座るフェイスシッティング',
'licking':'なめている・舌で触れている',
'tongue out':'舌を出している',
'lip licking':'唇を舌で舐める',
'penis licking':'ペニスを舌で舐める',
'ball licking':'睾丸を舌で舐める',
'licking shaft':'竿の部分を舐める',
'saliva':'唾液がつながっている描写',
'saliva string':'唾液の糸がつながっている',
'drooling on penis':'ペニスによだれを垂らしている',
'mouth full':'口いっぱいに含んでいる',
'bulge in cheek':'頬が膨らむほど含んでいる',
'spitting':'唾液を吐き出している',
'cum in mouth':'口の中に射精',
'swallowing cum':'精液を飲み込む',
'throat fuck':'喉奥を突く激しいイラマチオ',
'gagging':'えずきながら咥えている',
'teary eyes from oral':'フェラで目に涙が浮かぶ',
'sucking fingers':'指をしゃぶっている',
'nipple licking':'乳首を舌で舐める',
'nipple sucking':'乳首を吸う',
'breast licking':'胸を舌で舐める',
// === アナル ===
'anal':'アナルセックス・肛門性交',
'anal sex':'アナルセックスanalと同義',
'anal penetration':'肛門への挿入',
'anal insertion':'肛門に何かを挿入',
'deep anal':'奥深くまでアナル挿入',
'anal creampie':'肛門内中出し',
'cum in ass':'肛門内に中出し',
'double penetration':'膣と肛門に同時挿入',
'dp':'ダブルペネトレーションdpと同義',
'triple penetration':'3箇所に同時挿入',
'pegging':'ストラップオンで行うアナルセックス',
'strapon anal':'ストラップオンでのアナル挿入',
'finger in ass':'指でのアナル挿入',
'anal fingering':'指でアナルを刺激する',
'two fingers in ass':'2本の指でアナル挿入',
'butt plug':'アナルプラグを装着している',
'anal beads':'アナルビーズを使用',
'plug tail':'尻尾つきプラグを装着',
'object in ass':'物体を肛門に挿入',
'ass spread':'お尻を広げて肛門を見せている',
'spread ass':'お尻を手で広げた状態',
'gaping':'ぱっくりと開いた状態',
'gaping ass':'肛門がぱっくりと開いた状態',
'prolapse':'直腸脱・プロラプス描写',
// === 射精・体液 ===
'cum':'精液の描写',
'cumshot':'射精シーン',
'facial':'顔への射精・顔射',
'cum on face':'顔に精液がかかっている',
'cum on hair':'髪に精液がかかっている',
'cum on tongue':'舌の上に精液',
'cum dripping':'精液が滴り落ちている',
'dripping cum':'精液が垂れている',
'cum overflow':'精液があふれ出している',
'cum on breasts':'胸に精液がかかっている',
'cum on body':'体全体に精液がかかっている',
'cum on stomach':'お腹に精液',
'cum on back':'背中に精液',
'cum on thighs':'太ももに精液',
'cum on feet':'足に精液',
'multiple cumshots':'複数回の射精',
'massive cumshot':'大量射精',
'swallowing':'精液を飲み込む',
'cum in mouth':'口の中に射精',
'cum string':'精液の糸がつながっている',
'cum trail':'精液の跡が残っている',
'bukakke':'複数人から顔や体に射精するぶっかけ',
'covered in cum':'体が精液まみれ',
'messy cum':'精液で汚れた状態',
'sticky':'べたべたとした精液で汚れた状態',
'cum pool':'精液の池ができている',
'internal cumshot':'体内への射精断面図',
'x-ray cumshot':'X線で見た射精断面',
'womb full of cum':'子宮が精液でいっぱい',
'creampie':'膣内中出し',
// === 反応・表情 ===
'ahegao':'快感で目が虚ろ・舌が出たアヘ顔',
'rolling eyes':'快感で目が虚ろに上を向いた状態',
'eyes rolled back':'白目になるほどの快感',
'orgasm face':'絶頂時の恍惚な表情',
'climax face':'絶頂時の表情orgasm faceと同義',
'moaning':'口を開けて喘いでいる表情',
'mouth open':'口を開けている',
'panting':'荒い息づかい・はあはあしている',
'heavy breathing':'激しい呼吸・荒い息',
'gasping':'はっと息を飲む・あえぎ声',
'drooling':'よだれが垂れるほど放心した状態',
'blushing':'羞恥や興奮で頬が赤く染まる',
'full body blush':'全身が赤く染まるほど恥ずかしい',
'sweating':'全身に汗をかいている',
'crying':'涙を流して泣いている表情',
'tears':'涙が流れている',
'tears of pleasure':'快感で涙が出ている',
'teary eyes':'涙目になっている',
'shaking':'体が小刻みに震えている',
'trembling':'全身がガタガタ震える',
'twitching':'体が痙攣するように動く',
'convulsing':'けいれんしている',
'body spasm':'体が痙攣・スパズムしている',
'mind break':'快感で正気を失った精神崩壊',
'blank stare':'虚ろな目でぼーっとしている',
'dazed':'放心状態・ぼうっとしている',
'in heat':'発情中・欲情が抑えられない状態',
'lustful look':'欲情した目・官能的な眼差し',
'satisfied expression':'満足して気持ちよさそうな表情',
'ecstasy':'恍惚の絶頂感',
'euphoria':'この上ない幸福感・快楽',
'pleasure expression':'快感の表情',
'embarrassed moan':'恥ずかしそうに喘ぐ',
'trying to suppress moaning':'喘ぎ声を必死に抑えようとしている',
'biting lip':'唇を噛んで声を我慢している',
'biting sleeve':'袖を噛んで声を抑えている',
'ahegao aftermath':'アヘ顔の余韻・事後の虚ろ顔',
'post-orgasm':'絶頂後のぐったりした状態',
// === 衣装・脱衣 ===
'nude':'完全全裸',
'naked':'裸nudeと同義',
'topless':'上半身裸・胸が露出',
'bottomless':'下半身裸・下半身が露出',
'clothed female nude male':'服を着た女性と裸の男性',
'see-through':'透け透けの服',
'wet clothes':'濡れて体に張り付いた服',
'clothes pulled aside':'服を横にずらした状態',
'shirt lift':'シャツをめくり上げた状態',
'skirt lift':'スカートをめくった状態',
'lifting dress':'ドレスをたくし上げた状態',
'dress lift':'ドレスが持ち上がった状態',
'panties around ankles':'パンティが足首まで下がっている',
'panties pulled down':'パンティを引き下げた状態',
'bra pulled down':'ブラをずらして胸が出た状態',
'bra removed':'ブラを外した状態',
'lingerie':'セクシーなランジェリー',
'bra':'ブラジャー',
'panties':'パンティ',
'thong':'Tバックパンティ',
'g-string':'Gストリング・超細いパンティ',
'stockings':'ストッキング',
'garter belt':'ガーターベルト',
'corset':'コルセット',
'bodystocking':'全身タイツ系ボディストッキング',
'crotchless':'股部分がない衣装',
'sexy underwear':'セクシーな下着',
'ripped clothes':'破れた服',
'clothes torn':'服が引き裂かれた状態',
'undressing':'服を脱いでいる最中',
'lifting skirt':'スカートをたくし上げている',
'maid':'メイド服',
'school uniform':'学校の制服',
'sailor uniform':'セーラー服',
'gym uniform':'体操着',
'swimsuit':'水着',
'one-piece swimsuit':'ワンピース型水着',
'bikini':'ビキニ水着',
'bikini armor':'ビキニアーマー・際どい鎧',
'micro bikini':'極小ビキニ',
'kimono':'着物',
'yukata open':'浴衣がはだけた状態',
'bunny suit':'バニーガールスーツ',
'catsuit':'全身密着のキャットスーツ',
'latex':'ラテックス素材の密着服',
'leather outfit':'革製の衣装',
'fishnet bodysuit':'網目状のボディスーツ',
'fishnet stockings':'網タイツ',
'thigh-highs':'太ももまでのサイハイソックス',
'garter straps':'ガーターストラップ',
'crotchless lingerie':'股なしランジェリー',
'crotchless panties':'股なしパンティ',
'sheer lingerie':'透けて見えるランジェリー',
'sexy santa':'セクシーサンタコスチューム',
'sexy witch':'セクシー魔女コスチューム',
'nurse':'ナース服',
// === 拘束 ===
'bondage':'ボンデージ・拘束プレイ全般',
'rope bondage':'縄で縛るロープボンデージ',
'handcuffed':'手錠で手首を拘束',
'blindfold':'目隠しした状態',
'collar':'首輪をつけた状態',
'leash':'リード(鎖)でつながれた状態',
'ball gag':'ボールギャグをはめた状態',
'chains':'鎖で拘束された状態',
'tied up':'縛り上げられた状態',
'restrained':'自由を奪われた拘束状態',
'suspension bondage':'吊り下げた縛り・吊り縛り',
'hogtied':'後ろ手に縛られた体位',
'shibari':'縄縛り・紐が体に食い込む芸術縛り',
'vibrator bondage':'バイブを固定した拘束',
'ballgag':'ボールギャグball gagと同義',
'bit gag':'棒状のビットギャグ',
'ring gag':'口を開けたまま固定するリングギャグ',
'tape gag':'テープで口を塞いだ状態',
'chain bondage':'鎖での拘束',
'wrist cuffs':'手首の拘束具',
'ankle chain':'足首の鎖',
'ankle cuffs':'足首の拘束具',
'bound wrists':'手首を縛られた状態',
'bound ankles':'足首を縛られた状態',
'bound arms':'腕を縛られた状態',
'arms bound behind back':'後ろ手に縛られた状態',
'immobilized':'完全に動きを封じられた状態',
'tied to bed':'ベッドに縛り付けられた状態',
'tied to chair':'椅子に縛り付けられた状態',
'spread to bed':'ベッドに大の字で広げられた状態',
'spread eagle':'大の字に手足を広げた体勢',
'pulled by leash':'リードで引かれている状態',
'slave collar':'奴隷の証の首輪',
'neck collar':'首輪slave collarと同義',
// === 特殊シナリオ ===
'tentacle':'触手の描写',
'tentacle sex':'触手による性行為',
'tentacle rape':'触手による強制性行為',
'tentacle in mouth':'口に触手が入っている',
'tentacle in pussy':'膣に触手が入っている',
'monster sex':'モンスターとの性行為',
'goblin sex':'ゴブリンとの性行為',
'orc':'オークとの性行為',
'beast sex':'獣・動物との性行為',
'creature sex':'怪物・生物との性行為',
'futa':'ふたなりmale+female属性',
'futanari':'ふたなりfutaと同義',
'futa on female':'ふたなりが女性に挿入',
'futa on male':'ふたなりが男性に挿入',
'futa on futa':'ふたなり同士の性行為',
'yuri':'女性同士の恋愛・性行為',
'lesbian sex':'女性同士のセックス',
'tribadism':'女性同士が密着してこするトリバジズム',
'scissoring':'女性同士がはさみのように脚を絡める',
'yaoi':'男性同士の恋愛・性行為',
'male on male':'男性同士のセックス',
'gangbang':'複数人での輪姦',
'orgy':'大人数での乱交パーティー',
'threesome':'3人でのセックス',
'foursome':'4人でのセックス',
'ntr':'寝取られ・NTR',
'netorare':'寝取られntrと同義',
'femdom':'女性が支配的な女王様プレイ',
'submission':'支配される・服従',
'maledom':'男性が支配的なプレイ',
'domination':'力関係による支配プレイ',
'exhibitionism':'人に見られるのを楽しむ露出プレイ',
'public sex':'人目のある場所でのセックス',
'sex in public':'公衆の面前でのセックス',
'lactation':'母乳・授乳プレイ',
'breast milk':'母乳',
'milking':'搾乳・乳を絞る',
'pregnant':'妊娠した腹の描写',
'breeding':'孕ませプレイ・種付け',
'impregnation':'妊娠させる行為',
'internal view':'体内断面図',
'x-ray':'X線透過図',
'womb':'子宮の描写',
'groping':'胸や体をもみしだく',
'nipple suck':'乳首を吸っている',
'age difference':'年齢差のある組み合わせ',
'onee-shota':'年上お姉さん×少年のおねショタ',
'older man younger woman':'年上男性×年下女性',
'older woman younger man':'年上女性×年下男性',
'teacher and student':'教師と生徒の関係',
'affair':'不倫・浮気',
'cuckolding':'目の前で浮気を見せられる寝取り',
'wife sharing':'妻を他の男と共有',
'one night stand':'一夜限りの関係',
'secret sex':'秘密のセックス',
'forbidden love':'禁断の愛',
'reluctant':'嫌がりながらも・しぶしぶ',
'consensual':'合意の上で',
'forced':'強制的な行為',
'non-consensual implication':'非合意を示唆する描写',
'shotacon implication':'少年への性的関心を示唆',
'voyeurism':'覗き見・盗撮系の描写',
'glory hole':'壁の穴越しの性行為',
'outdoor sex':'屋外でのセックス',
'floor sex':'床の上でのセックス',
'morning sex':'朝目覚めてすぐのセックス',
'drunk sex':'酔った状態でのセックス',
'sleepy sex':'眠りながらのセックス',
'car sex':'車内でのセックス',
'bathroom sex':'バスルームでのセックス',
'locker room sex':'ロッカールームでのセックス',
'hot spring sex':'温泉でのセックス',
'harem':'複数人に囲まれたハーレム構図',
'reverse harem':'複数男性に囲まれる逆ハーレム',
// === ボディ詳細 ===
'feet':'足・足の描写',
'sole':'足の裏',
'toes':'足の指',
'armpits':'腋が見えている',
'inner thigh':'内太ももが見える',
'navel':'へそが見えている',
'flat stomach':'引き締まった平らなお腹',
'ass grab':'お尻をつかんでいる',
'ass spread':'お尻を広げて肛門を見せている',
'gaping ass':'肛門がぱっくりと開いた状態',
'sagging breasts':'垂れた胸',
'foot fetish':'足フェチ・足に注目した描写',
'armpit fetish':'腋フェチ・腋に注目した描写',
'belly fetish':'お腹フェチ・腹部に注目した描写',
'smell fetish':'匂いを嗅ぐフェチ描写',
'smell':'匂いを感じさせる描写',
'musk':'体臭・ムスクの香り',
'sweat fetish':'汗フェチ描写',
'sweaty body':'汗ばむ体',
'body odor fetish':'体臭フェチ描写',
'female ejaculation':'潮吹き・女性の射精',
'squirting':'潮吹きfemale ejaculationと同義',
'used':'使われた状態・ぐったり',
'well-used':'十分に使われてぐったりした状態',
'worn out':'疲れ果てた状態',
'exhausted after sex':'セックス後ぐったり疲弊した状態',
'after sex':'事後の描写',
'afterglow':'余韻の中でうっとりしている状態',
'used panties':'使用済みパンティ',
// === その他 ===
'rough sex':'激しく荒いセックス',
'gentle sex':'優しく丁寧なセックス',
'sex from behind':'後ろからのセックス',
'riding':'騎乗して乗る',
'pov':'一人称・主観視点',
'first person view':'主観視点povと同義',
'lap pillow':'膝枕',
'quickie':'素早いクイックセックス',
'strap-on sex':'ストラップオンを使ったセックス',
'strapon anal':'ストラップオンでのアナル挿入',
'pegging':'ストラップオンで行うアナルセックス',
'ahegao aftermath':'アヘ顔の余韻・事後の虚ろ顔',
'satisfied expression':'満足して気持ちよさそうな表情',
'overstimulation':'過剰な刺激で体が反応している',
'multiple orgasms':'何度も絶頂している',
'orgasm denial':'絶頂寸前で止められる焦らし',
'edging':'絶頂直前で止める寸止め',
'forced orgasm':'強制的に絶頂させられる',
'body spasm':'体が痙攣・スパズムしている',
'in heat':'発情中・欲情が抑えられない状態',
'too much pleasure':'快感が強すぎて耐えられない状態',
'female ejaculation':'潮吹き・女性の射精',
'squirting':'潮吹きfemale ejaculationと同義',
'swallowing cum':'精液を飲み込む',
'covered in cum':'体が精液まみれ',
'messy cum':'精液で汚れた状態',
'point of view penetration':'挿入シーンの主観視点',
'deep penetration':'深く奥まで挿入している',
// === 外見 ===
'white hair':'白髪',
'black hair':'黒髪',
'blonde hair':'金髪',
'brown hair':'茶髪',
'red hair':'赤髪',
'blue hair':'青髪',
'pink hair':'ピンク髪',
'purple hair':'紫髪',
'silver hair':'銀髪',
'gray hair':'グレー髪',
'green hair':'緑髪',
'orange hair':'オレンジ髪',
'multicolored hair':'複数色の混じった髪',
'gradient hair':'グラデーション髪',
'streaked hair':'ハイライトが入った髪',
'two-tone hair':'ツートンカラーの髪',
'long hair':'ロングヘア',
'short hair':'ショートヘア',
'medium hair':'ミディアムヘア',
'very long hair':'超ロングヘア・床まで届く長さ',
'twin tails':'ツインテール',
'ponytail':'ポニーテール',
'braid':'三つ編み',
'braided hair':'編み込みヘア',
'ahoge':'アホ毛・頭頂部に飛び出た一本毛',
'bangs':'前髪',
'sidelocks':'サイドロック・頬横の後れ毛',
'wavy hair':'ウェーブがかかった髪',
'curly hair':'くるくるとカールした髪',
'straight hair':'さらさらのストレートヘア',
'messy hair':'ぼさぼさに乱れた髪',
'hair bun':'お団子ヘア',
'double bun':'ダブルお団子',
'pigtails':'おさげ・サイドテール',
'red eyes':'赤い目',
'blue eyes':'青い目',
'green eyes':'緑の目',
'purple eyes':'紫の目',
'gold eyes':'金色の目',
'silver eyes':'銀色の目',
'brown eyes':'茶色の目',
'heterochromia':'左右の目の色が異なるオッドアイ',
'aqua eyes':'水色の目',
'pink eyes':'ピンクの目',
'yellow eyes':'黄色の目',
'white eyes':'白い目',
'glowing eyes':'発光している目',
'empty eyes':'感情がない虚ろな目',
'half-closed eyes':'眠そうに半開きの目',
'closed eyes':'目を閉じている',
'pale skin':'白く透き通るような肌',
'fair skin':'明るく綺麗な肌',
'tan skin':'小麦色に焼けた肌',
'dark skin':'黒い肌・浅黒い肌',
'white skin':'純白の肌',
'albino':'アルビノ・全体的に色素が薄い',
'white character':'全体的に白いキャラ',
'freckles':'そばかす',
'scar':'傷跡',
'birthmark':'あざ・生まれつきのマーク',
'elf ears':'エルフの尖った耳',
'cat ears':'猫耳',
'animal ears':'獣耳全般',
'horns':'角',
'halo':'頭上の光輪',
'wings':'翼',
'tail':'尻尾',
'demon girl':'悪魔の娘',
'angel':'天使',
'vampire':'吸血鬼',
'kemonomimi':'獣耳のあるキャラ・ケモ耳',
'fox ears':'狐耳',
'wolf ears':'狼耳',
// === nsfw_position 追加タグ ===
'missionary pov':'正常位の主観視点・見下ろしアングル',
'pov sex':'セックスの一人称・主観視点',
'from above pov':'上から見下ろした主観視点',
'reverse sitting':'後ろ向きに乗った体位',
'face sitting position':'顔に乗る・座位フェラ体位',
'lying on back':'仰向けに寝ている',
'lying on stomach':'うつ伏せに寝ている',
'lying on side':'横向きに寝ている',
'on all fours sex':'四つん這いでの体位',
'floor sex':'床の上でのセックス',
'bed sex':'ベッドの上でのセックス',
'spread to bed':'ベッドに大の字で広げられた状態',
'tied to bed sex':'ベッドに縛り付けられた状態でのセックス',
'leaning forward sex':'前傾みで寄りかかった体位',
'against desk':'机に寄りかかった体位',
'legs apart':'脚を左右に開いたポーズ',
'spread eagle':'大の字に手足を広げた体勢',
// === 外見サブタブ新規タグ ===
'very short hair':'超ショートヘア','high ponytail':'高い位置のポニテ',
'low ponytail':'低い位置のポニテ','side ponytail':'サイドポニテ',
'twin braids':'ツイン三つ編み','blunt bangs':'ぱっつん前髪',
'hair up':'アップスタイル','hair down':'おろし髪',
'hair flower':'ヘアフラワー','hair ribbon':'ヘアリボン',
'hair ornament':'ヘアアクセサリー','hair clip':'ヘアクリップ',
'bob cut':'ボブカット','pixie cut':'ピクシーカット','drill hair':'ドリルヘア',
'orange eyes':'オレンジ目','black eyes':'黒目','gray eyes':'グレー目',
'teal eyes':'青緑目','violet eyes':'バイオレット目',
'sparkling eyes':'キラキラした瞳','bedroom eyes':'とろんとした目',
'innocent eyes':'純粋な目','bags under eyes':'目の下のたるみ',
'dark circles':'クマ(目の下)','heavy eyelids':'重そうなまぶた',
'long eyelashes':'長いまつ毛','false eyelashes':'つけまつ毛','white eyelashes':'白いまつ毛',
'monocle':'片眼鏡(モノクル)','colored contact lenses':'カラーコンタクト',
'double eyelid':'二重まぶた','monolid':'一重まぶた',
'sharp eyes':'切れ長の目','soft eyes':'やわらかな目',
'round face':'丸顔','oval face':'面長','heart-shaped face':'ハート形の顔',
'square jaw':'角顎','pointed chin':'とがった顎',
'soft features':'柔らかい顔立ち','angular features':'シャープな顔立ち',
'sharp jaw':'シャープな輪郭','mole':'ほくろ',
'mole under eye':'泣きぼくろ','mole on cheek':'頬のほくろ',
'mole on neck':'首のほくろ','mole on breast':'胸のほくろ',
'blush stickers':'チーク(丸い赤み)','face paint':'フェイスペイント',
'button nose':'丸い鼻','upturned nose':'上向きの鼻','pointy nose':'尖った鼻',
'flat nose':'低い鼻','small nose':'小さな鼻',
'thin lips':'薄い唇','plump lips':'ぽってり唇','full lips':'厚い唇',
'small mouth':'小さな口','wide mouth':'大きな口',
'tongue piercing':'舌ピアス','lip piercing':'リップピアス',
'beauty mark':'ビューティーマーク',
'open mouth':'口を開けた','closed mouth':'口を閉じた',
'olive skin':'オリーブ肌','tanned':'日焼け肌','sunburn':'日焼け(赤み)',
'slim body':'細身の体型','slender':'スレンダー','athletic':'アスリート体型',
'hourglass figure':'砂時計体型','pear figure':'洋梨体型',
'visible collarbone':'鎖骨が見える','visible ribs':'肋骨が見える',
'long legs':'長い脚','short legs':'短い脚','slim legs':'細い脚',
'tattoo':'タトゥー','piercing':'ピアス','body piercing':'ボディピアス',
'scar on body':'体の傷跡',
// === appear_skin 新規タグ ===
'porcelain skin':'磁器のように白く滑らかな肌',
'milky skin':'ミルクのように白い柔らかな肌',
'translucent skin':'透き通るような透明感のある肌',
'tanned skin':'日焼けした小麦色の肌',
'healthy skin':'健康的な血色のよい肌',
'light-brown skin':'やや褐色の明るい茶色い肌',
'sun-kissed skin':'日光で軽く焼けたツヤ肌',
'bronzed skin':'ブロンズ色に焼けた肌',
'dark-skinned':'浅黒い・黒い肌のキャラ指定',
'black skin':'黒い肌',
'ebony skin':'黒檀のように深く黒い肌',
'smooth skin':'滑らかで凹凸のない肌',
'glowing skin':'光を帯びた輝く肌',
'soft skin':'柔らかでふわっとした肌感',
'rosy skin':'バラ色に上気した赤みのある肌',
'dewy skin':'みずみずしく潤った肌',
'satin skin':'サテンのようにツヤのある肌',
'matte skin':'マットで光沢を抑えた肌',
'freckled skin':'そばかすのある肌',
'glistening skin':'汗や光でぬれ光る肌',
'sweaty skin':'汗ばんだ肌',
// === appear_bodyshape 新規タグ ===
'thin':'細い・やせた体型',
'petite body':'小柄でコンパクトな体型',
'skinny':'骨ばった痩せ型の体型',
'lean body':'引き締まった筋肉質の体型',
'muscular':'筋肉質な体型',
'abs':'腹筋が見える・割れた腹筋',
'toned body':'引き締まった体型',
'fit body':'フィットした健康的な体型',
'broad shoulders':'幅広い肩',
'plump':'ぽっちゃりした体型',
'chubby':'ぽよぽよした丸みのある体型',
'soft body':'柔らかい肌感のある体型',
'voluptuous':'豊満でグラマラスな体型',
'plus size':'プラスサイズ・ふくよかな体型',
'large breasts':'大きな胸',
'medium breasts':'中程度の胸',
'small breasts':'小さな胸',
'flat chest':'平らな胸・貧乳',
'busty':'大きな胸が強調されたスタイル',
'wide hips':'広い腰まわり',
'narrow waist':'細いウエスト',
'big butt':'大きなお尻',
'flat butt':'平らなお尻',
'navel':'へそ・おへそ',
'belly button':'へそ',
'dimples of venus':'腰の上の二つのくぼみ',
'thick thighs':'太い太もも',
'thigh gap':'内ももに隙間がある',
// === appear_accessory 新規タグ ===
'necklace':'ネックレス',
'earrings':'イヤリング・ピアス(耳飾り)',
'bracelet':'ブレスレット',
'ring':'指輪',
'choker':'チョーカー(首輪風首飾り)',
'collar':'カラー・首輪',
'anklet':'アンクレット(足首の飾り)',
'armband':'アームバンド・腕輪',
'wristband':'リストバンド',
'body chain':'ボディチェーン(体に巻く鎖飾り)',
'belly ring':'へそピアス・べリーリング',
'tiara':'ティアラ(小型の冠)',
'crown':'王冠',
'headband':'ヘッドバンド',
'hair clip':'ヘアクリップ',
'hair ribbon':'ヘアリボン',
'hair bow':'髪につけるリボン',
'hair pin':'ヘアピン',
'scrunchie':'シュシュ(ヘアゴム)',
'veil':'ベール',
'hair accessory':'髪飾り全般',
'sunglasses':'サングラス',
'beret':'ベレー帽',
'hood':'フード付きの帽子・フード',
'gloves':'手袋',
'stockings':'ストッキング',
'fishnets':'フィッシュネット(網目タイツ)',
'mask':'マスク・仮面',
'badge':'バッジ',
'ribbon':'リボン',
'brooch':'ブローチ',
'watch':'腕時計',
'wristwatch':'腕時計',
// === ANI ポジティブ 髪まとめ ===
'tidy hair':'整った清潔感のある髪',
'neat hair':'きれいにまとまった髪',
'smooth hair':'滑らかでさらっとした髪',
'well-groomed hair':'手入れが行き届いた髪',
'sleek hair':'ツヤがありまとまった髪',
'perfect hair':'乱れのない完璧な髪',
'styled hair':'スタイリングされた髪',
'hair in place':'位置が固定された乱れない髪',
// === ANI ネガティブ 肌色・髪 ===
'sun-kissed':'軽く日焼けした肌(色白にしたい場合除外)',
'frizzy hair':'縮れた・うねった扱いにくい髪・弾く',
'messy hair':'乱れた髪・弾く',
'flyaway hair':'飛び散って広がった髪・弾く',
'bad hair day':'髪の調子が悪く乱れた状態・弾く',
'ahoge':'アホ毛・飛び出した一本毛・弾く',
'stray hair':'散らばった毛・飛び毛・弾く',
'unruly hair':'言うことを聞かない暴れた髪・弾く',
'wild hair':'激しく広がった野性的な髪・弾く',
'tall':'背が高い','average height':'平均身長',
'very tall':'かなり背が高い','extremely tall':'超長身',
'petite':'小柄な','towering':'そびえ立つ背丈',
'height difference':'身長差','small stature':'小さい体格',
'large stature':'大きい体格','tall girl':'背の高い女の子',
'short girl':'背の低い女の子','tall boy':'背の高い男の子',
'short boy':'背の低い男の子','loli':'ロリ体型','shota':'ショタ体型',
'adult body':'大人の体型','mature':'成熟した体つき',
'same height':'同じ身長','shorter than viewer':'視点より低身長',
'taller than viewer':'視点より高身長',
'dog ears':'犬耳','rabbit ears':'ウサギ耳','bear ears':'クマ耳',
'dragon ears':'ドラゴン耳','pointy ears':'尖った耳',
'dragon horns':'ドラゴンの角','oni horns':'鬼の角',
'demon horns':'悪魔の角','antlers':'鹿の角(枝角)',
'angel wings':'天使の翼','demon wings':'悪魔の翼',
'cat tail':'猫のしっぽ','fox tail':'狐のしっぽ',
'wolf tail':'狼のしっぽ','dragon tail':'ドラゴンのしっぽ',
'fluffy tail':'もふもふしっぽ','multiple tails':'複数のしっぽ',
'succubus':'サキュバス(夢魔)','witch':'魔女','elf':'エルフ',
'half-elf':'ハーフエルフ','ghost':'幽霊','zombie':'ゾンビ',
'mechanical parts':'機械パーツ(サイボーグ)','cyborg':'サイボーグ',
'scales':'鱗(うろこ)','fur':'毛皮','monster girl':'モンスター娘',
'dragon girl':'ドラゴン娘','naga':'ナーガ(蛇人間)','lamia':'ラミア',
'glowing markings':'発光する紋様','tribal markings':'部族の紋様',
'runes on body':'体に刻まれたルーン文字','dark sclera':'黒い白目',
// === 髪色・髪型新規タグ ===
'platinum hair':'プラチナシルバーの髪','dark blonde hair':'暗めの金髪',
'strawberry blonde hair':'イチゴブロンドの髪','light brown hair':'明るい茶髪',
'dark brown hair':'濃い茶髪','chestnut hair':'栗色の髪','auburn hair':'赤みがかった茶髪',
'dark red hair':'暗い赤髪','wine red hair':'ワインレッドの髪','cherry red hair':'チェリーレッドの髪',
'crimson hair':'深紅の髪','amber hair':'琥珀色の髪','copper hair':'銅色の髪',
'caramel hair':'キャラメル色の髪','peach hair':'ピーチ色の髪',
'hot pink hair':'濃いピンク髪','pale pink hair':'淡いピンク髪',
'rose pink hair':'ローズピンク髪','magenta hair':'マゼンタ色の髪',
'dark blue hair':'濃い青髪','navy blue hair':'ネイビーブルー髪',
'sky blue hair':'空色の髪','royal blue hair':'ロイヤルブルー髪',
'aqua hair':'水色がかった髪','teal hair':'ティール色の髪',
'turquoise hair':'ターコイズ色の髪','cyan hair':'シアン色の髪',
'dark green hair':'濃い緑髪','mint green hair':'ミントグリーン髪',
'lime green hair':'黄緑色の髪','forest green hair':'深緑の髪',
'dark purple hair':'濃い紫髪','lavender hair':'ラベンダー色の髪',
'lilac hair':'ライラック色の髪','violet hair':'バイオレット色の髪',
'indigo hair':'藍色の髪','golden hair':'ゴールドの髪',
'rainbow hair':'レインボーカラーの髪','ombre hair':'グラデーションヘア',
'highlighted hair':'ハイライトが入った髪','frosted tips':'毛先が白くなった髪',
'roots showing':'根本が染まっていない状態','dip-dyed hair':'毛先だけ染めたヘア',
'split color hair':'左右で色が違う髪','tricolor hair':'三色カラーの髪',
'extra long hair':'超ロングヘア','floor-length hair':'床まで届く超ロングヘア',
'waist-length hair':'腰まであるロングヘア','shoulder-length hair':'肩までのヘア',
'neck-length hair':'首の辺りの長さ','chin-length hair':'顎の長さのヘア',
'asymmetrical twintails':'左右非対称のツインテール',
'low twintails':'低めのツインテール','high twintails':'高めのツインテール',
'top knot':'頭の上でまとめたお団子','chignon':'シニョン・低い位置のお団子',
'half up':'ハーフアップ','half updo':'ハーフアップスタイル',
'crown braid':'頭頂部を一周する編み込み','odango':'お団子(中国風)',
'space buns':'耳の横にある二つのお団子',
'french braid':'フレンチブレイド','dutch braid':'ダッチブレイド',
'fishtail braid':'魚の尾のような編み込み','side braid':'横に垂らした三つ編み',
'over-shoulder braid':'肩越しに垂らした三つ編み',
'side swept bangs':'横に流した前髪','curtain bangs':'カーテンバング(センター分け)',
'see-through bangs':'透け感のある薄い前髪','no bangs':'前髪なし',
'middle part':'センターパート','side part':'サイドパート',
'loose curls':'ゆるいカール','spiral curls':'らせん状のカール',
'ringlets':'くるくるのたらし髪','kinked hair':'くせっ毛',
'fluffy hair':'ふわふわとした柔らかい髪','undercut':'サイド刈り上げヘア',
'shaggy hair':'シャギーカット','layered hair':'レイヤーカット',
'hairpin':'ヘアピン','barrette':'バレッタ','hair tie':'ヘアタイ',
'scrunchie':'シュシュ','hair bow':'ヘアボウ','hair band':'ヘアバンド',
'flower crown':'花冠','tiara on hair':'ヘアにつけたティアラ',
'hair beads':'ヘアビーズ','butterfly hair clip':'蝶々のヘアクリップ',
// === 手コキ・パイズリ・潮吹き ===
'handjob':'手コキ・手で男性器を刺激','stroking':'こすりあげる動作',
'two-handed handjob':'両手を使った手コキ','reverse handjob':'逆向き手コキ',
'footjob':'足コキ・足で男性器を刺激','foot on penis':'足が男性器に触れている',
'toes on penis':'足の指で刺激','sole on penis':'足の裏で刺激',
'thigh sex':'太もも性交','between thighs':'太ももの間に挟む',
'armpit sex':'腋性交・腋に挟む','armpit job':'腋コキ',
'male masturbation':'男性の自慰','stroking cock':'ペニスをこする',
'jerking off':'手で自慰する','paizuri':'パイズリ・胸で挟む',
'titjob':'チチコキpaizuriと同義','breast sex':'胸でのセックス',
'tit fuck':'胸コキ','paizuri from below':'下からのパイズリ',
'paizuri pov':'パイズリの主観視点',
'looking down at viewer paizuri':'見下ろしながらパイズリ',
'clothed paizuri':'服を着たままのパイズリ',
'paizuri and blowjob':'パイズリとフェラを同時に',
'double paizuri':'2人でパイズリ',
'small breast paizuri':'小さな胸でパイズリ','huge breast paizuri':'巨乳パイズリ',
'nipple play':'乳首を指で刺激するプレイ','nipple rub':'乳首をこする',
'nipple pinch':'乳首をつまむ','nipple twist':'乳首をねじる',
'nipple stimulation':'乳首への刺激','breast groping':'胸をもみしだく',
'squeezing breasts':'胸を強く握る',
// 潮吹き
'squirt':'潮吹き(動詞)','gushing':'どっと溢れる潮吹き',
'squirt on face':'顔に潮を吹きかける','squirt in mouth':'口に潮を吹きかける',
'massive squirt':'大量の潮吹き','squirt dripping':'潮が垂れている',
'squirt spray':'スプレー状の潮吹き','ahegao squirt':'アヘ顔で潮吹き',
'orgasm squirt':'絶頂時の潮吹き','squirt from fingering':'指マンで潮吹き',
'multiple squirts':'何度も潮吹き','wet spot':'濡れた跡・染み',
'soaked bed':'ベッドが濡れている',
// 百合
'girl on girl':'女の子同士','female on female':'女性同士の性行為',
'grinding together':'体をこすりつけ合う','double-ended dildo':'両挿しディルド',
'dildo sharing':'ディルドを共有','girls kissing':'女の子同士のキス',
'deep kiss girls':'女の子同士の濃厚なキス','tongue kiss':'舌を絡めるキス',
'breast fondling girls':'女の子同士で胸を触りあう',
'mutual touching':'互いに体を触れ合う','girls caressing':'女の子同士の愛撫',
'girls fingering':'女の子同士で指マン','mutual fingering':'互いに指マン',
'cunnilingus yuri':'百合のクンニ','romantic yuri':'恋愛的な百合',
'tender yuri':'優しく穏やかな百合','passionate yuri':'情熱的な百合',
'multiple girls sex':'複数の女の子でのセックス','yuri threesome':'百合の3人プレイ',
'girls orgy':'女の子たちの乱交',
// フタナリ
'dickgirl':'ディックガール(ふたなり別称)',
'female on futa':'女性がふたなりに行為','futa sex':'ふたなりのセックス',
'futa blowjob':'ふたなりにするフェラ','futa handjob':'ふたなりへの手コキ',
'futa paizuri':'ふたなりへのパイズリ','futa penetration':'ふたなりによる挿入',
'futa creampie':'ふたなりによる中出し','futa cumshot':'ふたなりの射精',
'futa bulge':'ふたなりの膨らみ','futa erection':'ふたなりの勃起',
'futa balls':'ふたなりの睾丸','huge futa cock':'巨大なふたなりペニス',
'small futa cock':'小さいふたなりペニス','futa masturbation':'ふたなりの自慰',
'futa solo':'ふたなり1人',
// モンスター・触手
'tentacle in ass':'肛門に触手が入っている','multiple tentacles':'複数の触手',
'tentacle wrap':'触手が体に巻きつく','restrained by tentacle':'触手に拘束される',
'tentacle creampie':'触手による中出し','monster penetration':'モンスターによる挿入',
'monster cock':'モンスターのペニス','non-human penis':'人間以外のペニス',
'machine sex':'機械とのセックス','mechanical penetration':'機械による挿入',
'robot sex':'ロボットとのセックス','android sex':'アンドロイドとのセックス',
'plant sex':'植物との性行為','vine sex':'ツルに絡まる性行為',
'alien sex':'エイリアンとのセックス',
// シナリオ
'stealing':'奪い取る行為','stolen sex':'盗むようなセックス',
'bdsm':'BDSM拘束・支配・奴隷・被虐プレイ',
'degradation':'貶める・辱める行為','humiliation':'恥辱・屈辱',
'coercion':'強要・脅迫による行為',
'cheating':'不倫・浮気','shotacon implication':'少年への性的関心を示唆',
'drunk sex':'酔った状態でのセックス','sleepy sex':'眠りながらのセックス',
'somnophilia implication':'眠っている相手への性的関心',
'morning sex':'朝のセックス','one night stand':'一夜限りの関係',
'quickie':'素早いクイックセックス',
'first time':'初めてのセックス','virgin':'処女・童貞',
'defloration':'処女膜が破られる初体験',
'bara':'マッチョな男性同士のプレイ',
// フェチ
'barefoot':'裸足','soles':'足の裏','licking feet':'足を舐める',
'smelling feet':'足の匂いを嗅ぐ','sucking toes':'足の指を吸う',
'wrinkled soles':'しわのある足の裏','foot worship':'足を崇める',
'tickling feet':'足をくすぐる','dirty feet':'汚れた足',
'feet in face':'顔に足を押しつける','foot lick':'足舐め',
'sole lick':'足の裏を舐める','toe lick':'足の指を舐める',
'smelling soles':'足の裏の匂いを嗅ぐ','foot sniff':'足の匂いを嗅ぐ',
'armpit sniffing':'腋の匂いを嗅ぐ','sweaty armpits':'汗ばんだ腋',
'underarm licking':'腋の下を舐める','smelling armpits':'腋の匂いを嗅ぐ',
'armpit worship':'腋を崇める','navel fetish':'へそフェチ',
'stomach kissing':'お腹にキスする','navel worship':'へそを崇める',
'belly button licking':'へそを舐める','tummy display':'お腹を見せる',
'belly rub':'お腹をなでる','nape fetish':'うなじフェチ',
'neck licking':'首を舐める','throat kissing':'のどにキスする',
'nape licking':'うなじを舐める','neck biting':'首を噛む',
'collar bone licking':'鎖骨を舐める','back of neck':'首の後ろ',
'ear licking':'耳を舐める','ear nibbling':'耳をかじる',
'ear whispering':'耳元でささやく','ear bite':'耳を噛む','earlobe lick':'耳たぶを舐める',
'hand fetish':'手フェチ','finger licking':'指を舐める',
'finger sucking':'指を吸う','gloves fetish':'手袋フェチ',
'long fingernails':'長い爪','nail fetish':'爪フェチ',
'ring fetish':'指輪フェチ','hand licking':'手を舐める',
'thigh fetish':'太もものフェチ','thigh gap':'太ももの隙間',
'thigh kiss':'太ももにキス','thigh lick':'太ももを舐める',
'thigh sniff':'太ももの匂いを嗅ぐ','thigh squeeze':'太ももを絞める',
'butt fetish':'お尻フェチ','spanking':'お尻を叩く',
'ass slap':'お尻を平手打ち','butt sniff':'お尻の匂いを嗅ぐ',
'ass massage':'お尻をマッサージ','ass bite':'お尻を噛む','hip worship':'腰を崇める',
'nipple fetish':'乳首フェチ','nipple torture':'乳首責め',
'nipple clamp':'乳首クリップ','nipple pull':'乳首を引っ張る',
'nipple chain':'乳首チェーン','nipple tickle':'乳首をくすぐる','nipple tweak':'乳首をひねる',
'erect nipples worship':'勃起した乳首を崇める','glistening sweat':'光るような汗',
'sweat droplets':'汗のしずく','panty sniffing':'パンツの匂いを嗅ぐ',
'saliva fetish':'唾液フェチ','drool':'よだれ','saliva play':'唾液プレイ',
'spit fetish':'唾フェチ','tongue display':'舌を見せる','wet tongue':'濡れた舌',
'thigh-high fetish':'サイハイフェチ','sock fetish':'靴下フェチ',
'stocking fetish':'ストッキングフェチ','knee-high socks':'ひざ上ソックス',
'ankle socks':'くるぶしソックス','bare legs':'素足・スッキリした脚',
'leg worship':'脚を崇める','fishnet fetish':'網タイツフェチ',
'megane':'眼鏡megane','adjusting glasses':'眼鏡をかけ直す',
'over glasses':'眼鏡越しに見る','uniform fetish':'制服フェチ',
'maid fetish':'メイド服フェチ','nurse fetish':'ナース服フェチ',
'school uniform fetish':'制服フェチ','gym clothes fetish':'体操着フェチ',
'swimsuit fetish':'水着フェチ','latex fetish':'ラテックスフェチ',
'back view':'後ろ姿','exposed back':'背中を露出した状態',
'spine view':'背骨が見える','dimples of venus':'背中のくぼみ(腰のえくぼ)',
'back muscles':'背中の筋肉','back worship':'背中を崇める','back lick':'背中を舐める',
// エロポーズ
'seductive pose':'誘惑的なポーズ','presenting':'体を見せびらかすポーズ',
'ass presenting':'お尻を突き出して見せるポーズ','spread pussy display':'開いた女性器を見せるポーズ',
'showing off body':'体を見せびらかす','come hither':'こっちにおいでと誘うポーズ',
'inviting pose':'誘うようなポーズ','alluring pose':'魅惑的なポーズ',
'legs spread wide':'脚を大きく広げた状態','m-shaped legs':'M字開脚',
'lying spread legs':'仰向けで脚を広げた状態','spread legs lying':'横になって脚を広げた',
'open legs display':'脚を開いて見せるポーズ',
'lying on back lewdly':'エッチな仰向けポーズ','legs in air':'足を空中に上げた状態',
'lying with legs apart':'足を開いて横たわる','supine lewd pose':'仰向けのエロポーズ',
'back on bed':'ベッドに背を向ける',
'lying face down lewd':'エッチなうつ伏せポーズ','prone lewd':'うつ伏せのエロポーズ',
'ass up lying':'横たわりながらお尻を突き出す','face down butt up':'顔を下げてお尻を上げた',
'lying prone seductive':'うつ伏せで誘惑するポーズ',
'lying on side lewd':'横向きのエロポーズ','side lying spread':'横向きで体を開く',
'side pose seductive':'横向きで誘惑するポーズ',
'seiza lewd':'正座でエッチなポーズ','sitting spread legs':'座って脚を開く',
'japanese sitting lewd':'正座・エッチなお座り','w-sit':'ペタ座り・W字の座り方',
'floor sitting spread':'床に座って脚を広げた','kneeling spread':'膝をついて開いた',
'standing spread legs':'立ったまま脚を開いた','arms up naked':'裸で腕を上げた',
'stretching nude':'裸でストレッチ','wall lean seductive':'壁に寄りかかって誘惑',
'hip thrust':'腰を突き出す','arched back standing':'立ったまま反り身',
'squatting lewd':'エッチなしゃがみポーズ','crouching seductive':'誘惑するようにかがむ',
'squat spread':'しゃがんで開いた状態','crouching display':'かがんで見せるポーズ',
'crouching naked':'裸でしゃがんだ状態',
'all fours presenting':'四つん這いで見せるポーズ','on all fours lewd':'エッチな四つん這い',
'ass up all fours':'四つん這いでお尻を上げた','crawling seductive':'四つん這いで誘惑',
'doggy display':'バック体位を見せるポーズ',
'mounting pose':'乗っかるようなポーズ','straddling pose':'またがるポーズ',
'riding position display':'騎乗位のポーズ',
'pressed against wall pose':'壁に押しつけられたポーズ',
'floor lying display':'床に横たわって見せるポーズ','back against wall':'壁に背をつける',
'wall spread':'壁に広げられた状態','pinned pose':'押さえつけられたポーズ',
'looking up at viewer':'見上げるカメラ目線','pov looking down':'上から見下ろした主観',
'eye contact pov':'目が合う主観視点',
'waist display':'腰を見せるポーズ','hip emphasis':'腰を強調したポーズ',
'body curve display':'体のラインを見せるポーズ','arched back lying':'横たわって反り身',
'chest out pose':'胸を突き出したポーズ',
'raised hips':'腰を持ち上げた状態','lifting one leg':'片脚を上げたポーズ',
'leg raised high':'高く脚を上げた状態','one leg up':'片脚を上げる',
'high kick pose':'高いキックのポーズ',
'full split':'前後開脚(フルスプリット)','side split':'横開脚',
'legs up high':'高く脚を上げた状態','maximum spread':'最大限に開いた状態',
'hugging pillow':'抱き枕を抱えている','body pillow hug':'抱き枕を抱きしめる',
'self-hug seductive':'自分を抱きしめて誘惑',
'sitting up seductive':'起き上がりながら誘惑','getting up pose':'起き上がるポーズ',
'stretching awake lewd':'寝起きエッチなストレッチ',
// === costume_casual タグ日本語訳 ===
// トップス
't-shirt':'Tシャツ・基本のカジュアルトップス',
'crop top':'ヘソ出しショートカットトップス',
'tank top':'タンクトップ・ノースリーブ',
'camisole':'キャミソール・細ストラップトップス',
'tube top':'チューブトップ・ビスチェ型トップス',
'off-shoulder top':'肩を出したオフショルダートップス',
'halter top':'ホルタートップ・首ひも・背中オープン',
'hoodie':'パーカー・フード付きスウェット',
'oversized hoodie':'ゆったり大きめサイズのパーカー',
'zip-up hoodie':'ファスナー式ジップアップパーカー',
'crop hoodie':'丈が短いクロップドパーカー',
'sweatshirt':'スウェットシャツ・裏起毛の長袖',
'sweater':'セーター・ニットのトップス',
'turtleneck sweater':'タートルネックセーター・首まで覆う',
'knit sweater':'ニット素材のセーター',
'cardigan':'カーディガン・前開きのニット',
'polo shirt':'ポロシャツ・衿付きカジュアルシャツ',
'blouse':'ブラウス・女性的なシャツ',
'button-up shirt':'ボタンで前を留めるシャツ',
'flannel shirt':'フランネルシャツ・チェック柄厚手シャツ',
'denim shirt':'デニム素材のシャツ',
'sleeveless shirt':'袖なしシャツ・ノースリーブシャツ',
'bodysuit':'ボディスーツ・体に密着した上半身服',
// ボトムス
'jeans':'ジーンズ・デニムパンツ',
'skinny jeans':'スキニージーンズ・細身タイトデニム',
'wide-leg pants':'ワイドパンツ・ゆったり裾広がりパンツ',
'shorts':'ショートパンツ',
'hot pants':'ホットパンツ・超短パンツ',
'denim shorts':'デニム素材のショートパンツ',
'cargo pants':'カーゴパンツ・横ポケット多いズボン',
'sweatpants':'スウェットパンツ・部屋着パンツ',
'yoga pants':'ヨガパンツ・体にフィットするパンツ',
'leggings':'レギンス・タイツ型のパンツ',
'track pants':'トラックパンツ・ジャージ下',
// スカート
'pleated skirt':'プリーツスカート・ひだ付きスカート',
'denim skirt':'デニムスカート',
'pencil skirt':'ペンシルスカート・体のラインを強調',
'wrap skirt':'ラップスカート・巻きスカート',
'A-line skirt':'Aラインスカート・ウエストから広がる',
'tiered skirt':'ティアードスカート・段々フリルスカート',
'ruffled skirt':'フリルスカート・ひらひら装飾スカート',
// ワンピース
'sundress':'サンドレス・夏のノースリーブワンピース',
'casual dress':'カジュアルなワンピース',
'shirt dress':'シャツ風ワンピース',
'wrap dress':'ウラップドレス・巻きつけタイプのドレス',
'slip dress':'スリップドレス・下着風シンプルワンピ',
'mini dress':'ミニ丈のドレス',
'maxi dress':'足首まであるロングワンピース',
'off-shoulder dress':'肩を出したオフショルダーワンピース',
'strapless dress':'ストラップなしのドレス',
'halter dress':'首ひもで留めるホルタードレス',
'backless dress':'背中が大きく開いたドレス',
// アウター
'denim jacket':'デニムジャケット・Gジャン',
'leather jacket':'レザージャケット・革ジャン',
'bomber jacket':'ボンバージャケット・MA-1系フライトジャケット',
'varsity jacket':'バーシティジャケット・スタジャン',
'trench coat':'トレンチコート・ベルト付きコート',
'peacoat':'ピーコート・ウール素材のショートコート',
'parka':'パーカーコート・防寒用フードコート',
'windbreaker':'ウィンドブレーカー・軽量防風アウター',
'raincoat':'レインコート・防水コート',
'fur coat':'ファーコート・毛皮コート',
'faux fur coat':'フェイクファーコート・合成毛皮コート',
'oversized coat':'オーバーサイズコート・ゆったり大きめ',
'blazer':'ブレザー・テーラードジャケット',
// セット
'overalls':'オーバーオール・サロペット',
'dungarees':'デニムオーバーオール',
'romper':'ロンパース・ショートパンツつなぎ',
'jumpsuit':'ジャンプスーツ・全身つなぎ',
// スポーツ
'gym clothes':'ジムウェア・スポーツ運動着',
'sportswear':'スポーツウェア・運動服全般',
'sports bra':'スポーツブラ・運動用ブラジャー',
'running shorts':'ランニング用ショーツ',
'track suit':'ジャージ上下セット・トラックスーツ',
'one-piece swimsuit':'ワンピース型水着・セパレートでない水着',
'competition swimsuit':'競泳水着・スポーツ用水着',
'gymnastics leotard':'体操競技用レオタード',
'volleyball uniform':'バレーボールユニフォーム',
'basketball jersey':'バスケットボールジャージ',
'soccer jersey':'サッカーユニフォーム',
'tennis outfit':'テニスウェア・テニス服',
'baseball uniform':'野球ユニフォーム',
'martial arts gi':'武道着・道着(柔道・空手等)',
'boxing shorts':'ボクシング用ショーツ',
'cycling shorts':'サイクリング用ショーツ・パッド入り',
'ski suit':'スキーウェア・防寒スキー服',
'wetsuit':'ウェットスーツ・サーフィン用スーツ',
'equestrian outfit':'乗馬服・馬術衣装',
// === costume_uniform タグ日本語訳 ===
// 学校制服
'gakuran':'学ラン・男子学生の詰め襟制服',
'summer school uniform':'夏用の学校制服・半袖',
'winter school uniform':'冬用の学校制服・長袖厚手',
'gym uniform':'体操服・学校の運動用制服',
'graduation hakama':'卒業式袴・女子の卒業式衣装',
// 医療・科学
'nurse uniform':'看護師のナース服・白衣スタイル',
'doctor coat':'医者の白衣・診察コート',
'lab coat':'実験室用白衣・研究者コート',
'scrubs':'術着・スクラブ(病院勤務者の着替え服)',
'surgeon gown':'手術衣・オペガウン',
'pharmacist coat':'薬剤師の白衣',
// 軍事・安全
'police uniform':'警察官の制服',
'military uniform':'軍服・ミリタリーユニフォーム',
'army uniform':'陸軍制服',
'navy uniform':'海軍制服',
'air force uniform':'空軍制服',
'firefighter uniform':'消防士の制服・防火服',
'security guard uniform':'警備員の制服',
'SWAT uniform':'SWAT特殊部隊の戦闘服',
'camouflage uniform':'迷彩服・カモフラージュ',
'soldier uniform':'兵士の制服・軍装',
// サービス
'flight attendant uniform':'客室乗務員の制服・CA服',
'pilot uniform':'パイロットの制服',
'chef uniform':'シェフ服・コックコート',
'waiter uniform':'ウェイターの制服',
'waitress uniform':'ウェイトレスの制服',
'barista apron':'バリスタのエプロン',
'maid uniform':'メイド服・メイドさん衣装',
'butler uniform':'執事服・バトラーの衣装',
'hotel staff uniform':'ホテルスタッフの制服',
'postal worker uniform':'郵便配達員の制服',
// ビジネス
'business suit':'ビジネススーツ',
'office lady outfit':'OL服・オフィスレディスタイル',
'lawyer suit':'弁護士スーツ',
'judge robe':'裁判官のローブ・法衣',
'construction worker outfit':'建設作業員の作業服',
'mechanic uniform':'整備士・修理工の作業着',
'factory worker uniform':'工場作業員の制服',
// フォーマル
'tuxedo':'タキシード・男性の礼服',
'formal suit':'フォーマルスーツ・礼装用スーツ',
'black tie outfit':'ブラックタイ正装(夜の礼装)',
'morning coat':'モーニングコート・昼の礼装',
'evening gown':'イブニングドレス・夜会用ドレス',
'ball gown':'ボールガウン・大きく広がった舞踏会ドレス',
'cocktail dress':'カクテルドレス・パーティードレス',
'prom dress':'プロムドレス・卒業パーティードレス',
'red carpet gown':'レッドカーペット用ドレス・式典ドレス',
// ウェディング
'wedding dress':'ウェディングドレス・花嫁のドレス',
'bridal dress':'花嫁衣装全般',
'bridesmaid dress':'ブライズメイドドレス・介添えドレス',
'bridal kimono':'白無垢・色打掛など花嫁和装',
// 日本伝統
'kimono':'着物・和服',
'furisode':'振袖・成人式などの正式着物',
'yukata':'浴衣・夏の和服・お祭り衣装',
'hakama':'袴・フォーマルな和装(ズボン型)',
'miko outfit':'巫女服・神社の巫女衣装',
'geisha outfit':'芸者・舞妓の衣装',
'haori':'羽織・着物の上に羽織るコート',
'tomesode':'留袖・既婚女性の正式な着物',
'houmongi':'訪問着・セミフォーマルな着物',
'komon':'小紋・カジュアルな柄着物',
'kunoichi outfit':'くノ一の衣装・女性忍者スタイル',
// アジア伝統
'hanbok':'韓国の伝統衣装ハンボク',
'qipao':'チャイナドレス・旗袍(中国伝統服)',
'cheongsam':'チョンサム・旗袍の別名(スリット入り)',
'tang suit':'唐装・中国のスーツ風伝統服',
'ao dai':'アオザイ・ベトナムの民族衣装',
// 宗教
'monk robe':'僧侶の法衣・袈裟',
'buddhist monk robe':'仏教僧侶の法衣・袈裟',
'priest outfit':'神父・司祭の聖職者服',
'nun outfit':'修道女・シスターの衣装',
'bishop robe':'司教の正装ローブ・高位聖職者',
'shinto priest outfit':'神道の神主・宮司の服装',
// ロリータ
'lolita dress':'ロリータドレス・フリルとリボンの衣装',
'gothic lolita':'ゴシックロリータ・黒基調ロリ服',
'sweet lolita':'スイートロリータ・パステルカラーロリ',
'classic lolita':'クラシックロリータ・上品な正統派ロリ',
'punk lolita':'パンクロリータ・ロック系ロリ服',
'hime lolita':'姫ロリータ・お姫様風ロリ服',
'sailor lolita':'セーラーロリータ・セーラー風ロリ',
// ダンス・ステージ
'ballet tutu':'バレエのチュチュスカート・フワフワ短スカート',
'ballet leotard':'バレエレオタード・体にフィットした舞踊服',
'ballroom dress':'社交ダンスドレス・広がるロングドレス',
'flamenco dress':'フラメンコドレス・スペイン伝統ダンス服',
'figure skating outfit':'フィギュアスケート衣装・スパンコール',
'belly dancer outfit':'ベリーダンサーの衣装・露出多め中東風',
'hula outfit':'フラダンスの衣装・ハワイアン',
'stage costume':'ステージ衣装・舞台衣装',
'idol costume':'アイドル衣装・推しコスチューム',
'cheerleader outfit':'チアリーダー衣装・ポンポン持ち',
// === costume_fantasy タグ日本語訳 ===
// 鎧・重装備
'knight armor':'騎士の全身鎧・ナイトアーマー',
'full plate armor':'フルプレートアーマー・完全板金鎧',
'chainmail armor':'チェーンメイル・鎖帷子・金属鎖の鎧',
'leather armor':'レザーアーマー・革製の軽量鎧',
'dark knight armor':'暗黒騎士の漆黒の鎧',
'paladin armor':'聖騎士パラディンの鎧・神聖な鎧',
'valkyrie armor':'ヴァルキリー・北欧神話戦乙女の鎧',
'dragon armor':'ドラゴンの鱗を使った鎧',
'demon armor':'悪魔の鎧・角つき甲冑',
'battle armor':'汎用バトルアーマー・戦闘用鎧',
'royal armor':'王族の装飾された豪華な鎧',
'holy knight armor':'聖騎士の白銀の鎧・光る神聖鎧',
'battle bikini armor':'露出の多い戦闘衣装・バトルビキニアーマー',
// 魔法使い系
'wizard robe':'魔法使いのローブ(帽子付きスタイル)',
'mage robe':'メイジのマントローブ',
'witch outfit':'魔女の衣装(帽子・黒ドレス)',
'warlock robe':'ウォーロック・暗黒魔法使いのローブ',
'sorcerer robe':'ソーサラーの豪華なローブ',
'enchanter robe':'エンチャンターの輝く魔法のローブ',
'necromancer robe':'ネクロマンサーの黒いローブ・死霊魔術師',
'summoner robe':'召喚師のローブ',
'druid robe':'ドルイドの自然系ローブ・緑の魔法使い',
'oracle robe':'神官・神託師の白いローブ',
'magical girl outfit':'魔法少女の可愛い変身衣装',
'mahou shoujo':'魔法少女スタイル・日本アニメ風変身衣装',
// 戦士・盗賊
'ranger outfit':'レンジャーの革製の装備・弓使い',
'archer outfit':'アーチャーの軽装衣装・弓兵',
'rogue outfit':'ローグ・泥棒の影に潜む衣装',
'assassin outfit':'アサシン・暗殺者の黒い衣装',
'thief outfit':'シーフ・盗賊の軽装衣装',
'berserker armor':'バーサーカーの重装備・荒々しい戦士鎧',
'barbarian outfit':'バーバリアンの原始的な衣装',
'warrior outfit':'戦士の基本的な戦闘衣装',
'mercenary outfit':'傭兵の実用的な装備',
'adventurer outfit':'冒険者の旅装・RPG冒険者スタイル',
'hero outfit':'勇者の衣装・定番RPG主人公スタイル',
'adventurer cloak':'冒険者マント・旅人のクローク',
// 特殊・超自然
'angel outfit':'天使の白い衣装・翼付き',
'fallen angel outfit':'堕天使の衣装・黒翼付き',
'demon outfit':'悪魔の衣装・角と翼',
'vampire outfit':'吸血鬼の衣装・マント付き',
'succubus outfit':'サキュバスの誘惑的な衣装',
'ghost outfit':'幽霊の衣装・白い布',
'zombie outfit':'ゾンビの衣装・ボロボロの服',
'fairy outfit':'妖精の可愛い衣装・翅付き',
'elf outfit':'エルフの衣装・森の民スタイル',
'exorcist outfit':'祓魔師・エクソシストの衣装',
'onmyoji outfit':'陰陽師の衣装・和風呪術師スタイル',
'shaman outfit':'シャーマン・祈祷師の衣装',
// 歴史・民族
'samurai armor':'侍の甲冑・武士の鎧',
'viking armor':'ヴァイキングの北欧戦士鎧',
'gladiator outfit':'グラディエーター・剣闘士の衣装',
'roman armor':'ローマ兵の鎧・古代ローマ軍装',
'greek warrior outfit':'古代ギリシャ戦士の衣装',
'spartan armor':'スパルタ兵の赤マント鎧',
'egyptian outfit':'古代エジプト衣装',
'pharaoh outfit':'ファラオの王の衣装・黄金の冠と白衣装',
'medieval dress':'中世ヨーロッパのドレス',
'renaissance dress':'ルネサンス時代の豪華なドレス',
'baroque dress':'バロック様式の宮廷ドレス',
'victorian dress':'ビクトリア朝のドレス・19世紀英国風',
'rococo dress':'ロコロ様式の繊細な宮廷ドレス',
'ancient greek dress':'古代ギリシャの衣装・ペプロス・ドレープ',
'native american outfit':'ネイティブアメリカンの伝統衣装',
// SF・サイバーパンク
'space suit':'宇宙服・宇宙探査スーツ',
'astronaut suit':'宇宙飛行士スーツ・NASA風スーツ',
'sci-fi armor':'SFアーマー・未来の高技術甲冑',
'cyberpunk outfit':'サイバーパンク衣装・ネオン未来都市スタイル',
'neon cyberpunk outfit':'ネオンに光るサイバーパンク衣装',
'android outfit':'アンドロイドの衣装・人型ロボット服',
'power armor':'パワードアーマー・強化外骨格スーツ',
'tactical gear':'タクティカルギア・特殊部隊装備一式',
'hazmat suit':'ハザマットスーツ・生物化学防護服',
'mecha suit':'メカスーツ・ロボット搭乗衣装',
'futuristic uniform':'未来的なデザインのユニフォーム',
'hacker outfit':'ハッカーの衣装・サイバー系スタイル',
// コスプレ・イベント
'bunny suit':'バニースーツ・バニー衣装(耳付き)',
'bunny girl outfit':'バニーガール衣装・耳付きセクシースーツ',
'cat girl outfit':'猫娘の衣装・耳と尻尾付き',
'santa outfit':'サンタクロース衣装',
'christmas outfit':'クリスマスコスチューム全般',
'halloween costume':'ハロウィン仮装衣装',
'pirate outfit':'海賊の衣装・帽子とベスト',
'cowboy outfit':'カウボーイ衣装・帽子とブーツ',
'western outfit':'西部劇衣装・ウエスタンスタイル',
'clown outfit':'ピエロ衣装・道化師の派手な衣装',
'ringmaster outfit':'サーカスの団長衣装・燕尾服と帽子',
'magician outfit':'マジシャンの衣装・タキシードとシルクハット',
'circus outfit':'サーカス衣装全般',
'court jester outfit':'道化師の宮廷衣装・鈴つきとんがり帽子',
'steampunk outfit':'スチームパンク衣装・蒸気機関×未来ファッション',
'gothic outfit':'ゴシック衣装・黒基調の耽美スタイル',
// ファンタジー王族
'princess dress':'プリンセスドレス・姫の華やかな衣装',
'queen gown':'女王のガウン・威厳ある豪華ドレス',
'noble outfit':'貴族の衣装・宮廷スタイル',
'royal court dress':'宮廷ドレス・王宮の正装',
'fairy tale princess':'おとぎ話の姫の衣装',
'dark queen outfit':'闇の女王衣装・黒と紫の豪華ドレス',
'elf queen outfit':'エルフの女王の森の衣装',
// ランジェリー・寝巻き
'lace lingerie':'レース素材のランジェリー',
'corset':'コルセット・ウエスト締め付け下着',
'babydoll':'ベビードール・透け感のある短いナイトドレス',
'nightgown':'ナイトガウン・寝間着ドレス',
'silk pajamas':'シルクパジャマ・つるつる素材のパジャマ',
'bathrobe':'バスローブ・お風呂上がりのローブ',
'negligee':'ネグリジェ・薄く透けたナイトウェア',
// === 新規キャラクター日本語訳 ===
'hoshino ai':'星野アイ(推しの子)','hoshino ruby':'星野ルビー(推しの子)',
'arima kana':'有馬かな(推しの子)','aquamarine hoshino':'星野アクア(推しの子)',
'iwakura lain':'岩倉玲音lain',
'gasai yuno':'我妻由乃(未来日記)','amano yukiteru':'天野雪輝(未来日記)',
'minene uryuu':'雨流みねね(未来日記)',
'revy':'レヴィBLACK LAGOON','roberta (black lagoon)':'ロベルタBLACK LAGOON',
'balalaika':'バラライカBLACK LAGOON',
'lucy (elfen lied)':'ルーシー/ニュウ(エルフェンリート)',
'nana (elfen lied)':'ナナ(エルフェンリート)',
'rakka':'落下(灰羽連盟)','reki (haibane renmei)':'礫(灰羽連盟)',
'kana (haibane renmei)':'佳奈(灰羽連盟)',
'alucard':'アーカードHELLSING','seras victoria':'セラス・ヴィクトリアHELLSING',
'integra hellsing':'インテグラ・ヘルシング',
'paprika (movie)':'パプリカ(映画)','mima kirigoe':'霧越未麻PERFECT BLUE',
'kaneda shotaro':'金田正太郎AKIRA','tetsuo shima':'島鉄雄AKIRA',
'faye valentine':'フェイ・バレンタイン(カウボーイビバップ)',
'edward (cowboy bebop)':'エドワード(カウボーイビバップ)',
'lina inverse':'リナ・インバース(スレイヤーズ)',
'naga the serpent':'ナーガ(スレイヤーズ)',
'zelgadis greywords':'ゼルガディス(スレイヤーズ)',
'shana':'シャナ(灼眼のシャナ)',
'furude rika':'古手梨花(ひぐらし)','rena ryuuguu':'竜宮レナ(ひぐらし)',
'hanyuu':'羽入(ひぐらし)','sonozaki mion':'園崎魅音(ひぐらし)',
'sonozaki shion':'園崎詩音(ひぐらし)','houjou satoko':'北条沙都子(ひぐらし)',
'b. jenet':'B・ジェニー餓狼',
'shiranui mai':'不知火舞KOF','athena asamiya':'麻宮アテナKOF',
'leona heidern':'レオナKOF','blue mary':'ブルー・マリーKOF',
'angel (kof)':'エンジェルKOF','kula diamond':'クーラ・ダイアモンドKOF',
'morrigan aensland':'モリガン・アーンスランド(ヴァンパイア)',
'lilith aensland':'リリス・アーンスランド(ヴァンパイア)',
'felicia (darkstalkers)':'フェリシア(ヴァンパイア)',
'hsien-ko':'レイレイ(ヴァンパイア)',
'i-no':'イGUILTY GEAR','baiken':'梅喧GUILTY GEAR',
'millia rage':'ミリア・レイジGUILTY GEAR',
'dizzy (guilty gear)':'ディジーGUILTY GEAR',
'bridget (guilty gear)':'ブリジットGUILTY GEAR',
'ramlethal valentine':'ラムレザル・ヴァレンタインGG',
'elphelt valentine':'エルフェルト・ヴァレンタインGG',
'sol badguy':'ソル・バッドガイGUILTY GEAR',
'ky kiske':'カイ・キスクGUILTY GEAR',
'valentine (skullgirls)':'ヴァレンタイン(スカルガールズ)',
'parasoul':'パラソウル(スカルガールズ)',
'filia (skullgirls)':'フィリア(スカルガールズ)',
'arcueid brunestud':'アルクェイド・ブリュンスタッド(月姫)',
'akiha tohno':'遠野秋葉(月姫)','hisui (tsukihime)':'翡翠(月姫)',
'kohaku (tsukihime)':'琥珀(月姫)','ciel (tsukihime)':'シエル(月姫)',
'shiki tohno':'遠野志貴(月姫)',
'beatrice (umineko)':'ベアトリーチェ(うみねこ)',
'bernkastel':'ベルンカステル(うみねこ)',
'lambdadelta':'ラムダデルタ(うみねこ)',
'ange ushiromiya':'右代宮縁寿(うみねこ)',
'rinslet walker':'リンスレット・ウォーカーBLACK CAT',
'panty (psg)':'パンティP&S','stocking (psg)':'ストッキングP&S',
'kneesocks (psg)':'ニーソックスP&S','scanty (psg)':'スキャンティP&S',
'nikaido (dorohedoro)':'ニカイドウ(ドロヘドロ)',
'ebisu (dorohedoro)':'エビス(ドロヘドロ)','noi (dorohedoro)':'ノイ(ドロヘドロ)',
'holly blue agate':'ホリー・ブルー・アゲートSU',
'peridot (steven universe)':'ペリドットSU',
'lapis lazuli (steven universe)':'ラピスラズリSU',
'kasumi (doa)':'霞DEAD OR ALIVE','ayane (doa)':'あやねDEAD OR ALIVE',
'marie rose (doa)':'マリーローズDEAD OR ALIVE',
'princess peach':'ピーチ姫(マリオ)','rosalina':'ロゼッタ(マリオ)',
'bowsette':'クッパ姫(マリオ二次創作)',
'saber (fate)':'セイバーFate','rin tohsaka':'遠坂凛Fate',
'sakura matou':'間桐桜Fate','shirou emiya':'衛宮士郎Fate',
'gilgamesh (fate)':'ギルガメッシュFate',
'illyasviel von einzbern':'イリヤスフィール・フォン・アインツベルンFate',
'frieren':'フリーレン(葬送のフリーレン)',
'fern':'フェルン(葬送のフリーレン)','stark':'シュタルク(葬送のフリーレン)',
'march 7th':'三月七日(崩壊:スターレイル)',
'kafka (honkai: star rail)':'カフカ(崩壊:スターレイル)',
'nahida (genshin impact)':'ナヒーダ(原神)',
'furina (genshin impact)':'フリーナ(原神)',
'todoroki shoto':'轟焦凍(僕のヒーローアカデミア)',
'bakugou katsuki':'爆豪勝己(僕のヒーローアカデミア)',
'kitagawa marin':'喜多川海夢(その着せ恋)',
'anya forger':'アーニャ・フォージャーSPY×FAMILY',
'amane suzuha':'阿万音鈴羽(シュタインズ・ゲート)',
// === 実写・写真品質 ===
'photorealistic':'フォトリアル・写真のようにリアル',
'hyperrealistic':'超高精細リアル・毛穴まで見える',
'ultra realistic':'超リアル・実写に限りなく近い',
'RAW photo':'RAW写真・撮って出しの質感',
'DSLR photo':'一眼レフカメラで撮影',
'mirrorless camera photo':'ミラーレスカメラで撮影',
'professional photography':'プロカメラマンによる撮影',
'portrait photography':'ポートレート写真・人物撮影',
'street photography':'ストリートフォト・街頭スナップ',
'fashion photography':'ファッション写真撮影',
'cinematic photography':'映画的な構図と色調',
'lifestyle photography':'ライフスタイル系・日常的な雰囲気',
'8k uhd':'8K超高解像度',
'4k resolution':'4K解像度',
'full HD':'フルHD解像度',
'high resolution':'高解像度',
'sharp focus':'くっきりピント・鮮明',
'bokeh':'ボケ・背景がぼんやり',
'shallow depth of field':'浅い被写界深度・ボケが強い',
'deep depth of field':'深い被写界深度・全体がシャープ',
'35mm film':'35mmフィルムの質感',
'50mm lens':'標準50mmレンズ・自然な歪みなし',
'85mm portrait lens':'ポートレートに最適な85mmレンズ',
'telephoto lens':'望遠レンズ・圧縮効果あり',
'Canon EOS R5':'キヤノン EOS R5で撮影',
'Sony α7 III':'ソニー α7 IIIで撮影',
'Nikon Z9':'ニコン Z9で撮影',
'film grain':'フィルム粒状感・レトロな質感',
'analog film':'アナログフィルム感',
'high ISO':'高ISO・暗所撮影のイズ感',
'long exposure':'長時間露光',
'studio flash':'スタジオフラッシュ使用',
'ring light':'リングライト使用',
'natural light photo':'自然光で撮影',
// === 実写・写真ネガティブ ===
'drawn':'手描き(排除)',
'anime':'アニメ調(排除)',
'illustration':'イラスト(排除)',
'cartoon':'カートゥーン(排除)',
'painting':'絵画タッチ(排除)',
'sketch':'スケッチ(排除)',
'watercolor':'水彩(排除)',
'CGI':'CGレンダリング排除',
'3D render':'3Dレンダー排除',
'3D model':'3Dモデル排除',
'artificial':'人工的・不自然(排除)',
'plastic skin':'プラスチックのような肌(排除)',
'doll-like':'人形のような(排除)',
'wax figure':'蝋人形のような(排除)',
'bad anatomy':'解剖学的に不正確(排除)',
'deformed':'変形・歪み(排除)',
'distorted':'歪んだ(排除)',
'disfigured':'醜く変形(排除)',
'mutant':'異形・突然変異(排除)',
'extra limbs':'余分な手足(排除)',
'extra fingers':'余分な指(排除)',
'missing fingers':'指が欠けている(排除)',
'fused fingers':'指が融合(排除)',
'long neck':'首が異常に長い(排除)',
'blurry':'ぼやけた(排除)',
'out of focus':'ピンぼけ(排除)',
'motion blur':'動きブレ(排除)',
'noise':'ノイズ(排除)',
'overexposed':'露出オーバー(排除)',
'underexposed':'露出アンダー(排除)',
'low contrast':'コントラスト不足(排除)',
'flat lighting':'のっぺりした照明(排除)',
'harsh shadows':'きつすぎる影(排除)',
'worst quality':'最低品質(排除)',
'low quality':'低品質(排除)',
'bad quality':'悪い品質(排除)',
'poor quality':'粗悪な品質(排除)',
'jpeg artifacts':'JPEGのブロックイズ排除',
'text':'テキスト(排除)',
'watermark':'ウォーターマーク(排除)',
'signature':'署名(排除)',
'border':'フレーム枠(排除)',
'frame':'額縁・フレーム(排除)',
'logo':'ロゴ(排除)',
// === 実写・被写体 ===
'1 girl':'女性1人',
'1 woman':'女性大人1人',
'1 boy':'男性少年1人',
'1 man':'男性大人1人',
'2 girls':'女性2人',
'couple':'カップル・2人連れ',
'group of people':'グループ・複数人',
'Japanese girl':'日本人女性',
'Japanese woman':'日本人女性(大人)',
'Japanese schoolgirl':'日本の女子高校生',
'high school girl':'女子高校生',
'college student':'大学生',
'office lady':'OL・会社員女性',
'young woman':'若い女性',
'teenager':'ティーンエイジャー',
'smiling':'微笑んでいる',
'serious expression':'真剣な表情',
'shy':'恥ずかしそう・はにかんだ',
'confident':'自信に満ちた',
'natural expression':'自然な表情',
'standing':'立っている',
'sitting':'座っている',
'walking':'歩いている',
'running':'走っている',
'looking back':'振り返っている',
'eyes closed':'目を閉じている',
// === 実写・ロケーション ===
'outdoors':'屋外',
'street':'街路・ストリート',
'city street':'都市の街路',
'urban':'都市部・都会的',
'downtown':'繁華街・中心街',
'suburban':'郊外',
'park':'公園',
'garden':'庭園',
'riverside':'川沿い・河川敷',
'bridge':'橋',
'rooftop':'屋上',
'school building exterior':'学校の校舎(外観)',
'school gate':'学校の正門',
'school courtyard':'校庭',
'train station':'駅・電車の駅',
'on the train':'電車の中',
'bus stop':'バス停',
'shopping mall':'ショッピングモール',
'beach':'海岸・ビーチ',
'seaside':'海辺・海岸沿い',
'mountain path':'山道・登山道',
'forest':'森・林',
'nature':'自然の中',
'indoors':'屋内',
'cafe':'カフェ',
'coffee shop':'コーヒーショップ',
'restaurant':'レストラン',
'library':'図書館',
'classroom':'教室',
'school hallway':'学校の廊下',
'convenience store':'コンビニ',
'bedroom':'寝室',
'living room':'リビング・居間',
'blurred background':'背景ぼかし・ボケ背景',
'bokeh background':'ボケた背景',
'simple background':'シンプルな背景',
'plain background':'無地の背景',
'out of focus background':'ピンぼけ背景',
'overcast sky':'曇り空',
'clear sky':'晴天・青空',
'night sky':'夜空',
'golden hour sky':'黄金時間帯の空',
// === 実写・制服・学校 ===
'from behind':'後ろ姿・後ろから',
'back view':'後ろ姿',
'long wavy hair':'ロングウェーブヘア',
'long wavy brown hair':'長いウェーブブラウンヘア',
'black blazer':'黒ブレザー',
'navy blazer':'ネイビーブレザー',
'gray blazer':'グレーブレザー',
'pleated mini skirt':'プリーツミニスカート',
'plaid skirt':'チェック柄スカート',
'dark plaid pattern':'暗いチェック柄',
'checkered skirt':'格子柄スカート',
'spandex shorts under skirt':'スカートの下のスパンデックスショーツ',
'black shorts underneath':'下に着た黒ショーツ',
'compression shorts':'コンプレッションショーツ・スパッツ',
'loose white socks':'白いルーズソックス',
'loose socks':'ルーズソックス',
'kogal style socks':'コギャルスタイルのソックス',
'over-knee socks':'オーバーニーソックス',
'brown loafers':'ブラウンのローファー',
'black loafers':'黒ローファー',
'loafers':'ローファー',
'bright pink bag':'鮮やかなピンクのバッグ',
'Boston bag':'ボストンバッグ',
'shoulder strap bag':'肩ひも付きバッグ',
'pink school bag':'ピンクのスクールバッグ',
'school uniform':'制服',
'Japanese school uniform':'日本の学校制服',
'white dress shirt':'白いドレスシャツ',
'necktie':'ネクタイ',
'ribbon bow':'リボン',
'sailor uniform':'セーラー服',
'sailor collar':'セーラーカラー',
'summer uniform':'夏服',
'winter uniform':'冬服',
'gym uniform':'体育服',
'mary jane shoes':'メリージェーンシューズ',
'white socks':'白ソックス',
'knee-high socks':'ひざ下ソックス',
'backpack':'リュックサック',
// === 実写・照明 ===
'natural lighting':'自然光',
'soft natural light':'柔らかい自然光',
'harsh sunlight':'強い直射日光',
'dappled light':'木漏れ日',
'studio lighting':'スタジオ照明',
'softbox lighting':'ソフトボックス照明',
'LED panel light':'LEDパネルライト',
'golden hour light':'ゴールデンアワー・夕焼け前後の光',
'sunset light':'夕陽の光',
'sunrise light':'朝焼けの光',
'blue hour':'ブルーアワー・日没直後の青い時間',
'backlit':'逆光',
'rim lighting':'リムライト・輪郭を際立てる光',
'contre-jour':'コントル・ジュール・逆光技法',
'halo lighting':'ハロー照明・後光のような光',
'window light':'窓からの自然光',
'indoor fluorescent light':'室内蛍光灯',
'ambient light':'環境光・間接照明',
'overcast sky lighting':'曇り空の拡散光',
'cloudy diffuse light':'雲で拡散した柔らかい光',
'dramatic lighting':'ドラマチックな照明',
'low key lighting':'ローキー照明・暗め・陰影強調',
'high key lighting':'ハイキー照明・明るめ・影が少ない',
'moody lighting':'ムーディーな照明・雰囲気重視',
'warm light':'暖色系の光',
'cool light':'寒色系の光',
'neutral light':'中性色の光',
'hard shadow':'くっきりした硬い影',
'soft shadow':'柔らかい影',
'no shadow':'影なし',
'long shadow':'長い影',
};
/* ===== STATE ===== */
let currentPreset = 'animagine';
let selectedTags = []; // [{tag, preset}] ポジティブ
let negSelectedTags = []; // [{tag, preset}] ネガティブプリセット選択分
let lockedNsfwTags = new Set(); // 入れ替えで変更しないNSFWタグ
let translatedText = '';
let translatedNegText = '';
let history = [];
let saves = (()=>{ try{ return JSON.parse(localStorage.getItem('pf_saves')||'[]')||[]; }catch(e){ return []; } })();
let searchQuery = '';
let dragSrcIdx = null;
let groupOrder = []; // カスタムグループ並び順(空=priorityデフォルト
let apiKey = (()=>{ try{ return localStorage.getItem('pf_apikey')||''; }catch(e){ return ''; } })();
let pendingImageData = null;
let addImgTargetIdx = null;
let nsfwAllPicked = []; // 全タブ一括で追加されたタグ記録(トグル用)
let currentCharaSeries = ''; // 現在選択中の作品キー
/* ===== API KEY ===== */
function onKeyInput() {
apiKey = document.getElementById('apikeyInput').value.trim();
try{ localStorage.setItem('pf_apikey', apiKey); }catch(e){}
updateKeyStatus();
}
function updateKeyStatus() {
const st = document.getElementById('keyStatus');
if (apiKey.startsWith('sk-ant')) { st.textContent = '✓ 設定済'; st.className = 'apikey-status ok'; }
else { st.textContent = '未設定'; st.className = 'apikey-status no'; }
}
/* ===== SEARCH ===== */
function onSearch() {
searchQuery = document.getElementById('searchInput').value.trim();
const tab = document.getElementById('searchTab');
if (searchQuery) { tab.style.display = ''; switchPreset('_search'); }
else { tab.style.display = 'none'; switchPreset('animagine'); }
}
function clearSearch() {
document.getElementById('searchInput').value = '';
searchQuery = '';
document.getElementById('searchTab').style.display = 'none';
switchPreset('animagine');
}
/* ===== TABS ===== */
function switchPreset(key) {
currentPreset = key;
document.querySelectorAll('.ptab').forEach(t => {
const m = t.getAttribute('onclick')?.match(/switchPreset\('([^']+)'\)/);
t.classList.toggle('active', m ? m[1] === key : false);
});
const randBtn = document.getElementById('randOneBtn');
if (randBtn) randBtn.style.display = key.startsWith('nsfw_') ? '' : 'none';
const csRow = document.getElementById('charaSeriesRow');
if (csRow) csRow.style.display = key === '_chara' || key === 'chara' ? '' : 'none';
const apRow = document.getElementById('appearSubRow');
if (apRow) apRow.style.display = key.startsWith('appear') ? '' : 'none';
const csRow2 = document.getElementById('costumeSubRow');
if (csRow2) csRow2.style.display = key.startsWith('costume') ? '' : 'none';
updateAllBtn(); renderTags();
}
function onCharaSeriesChange() {
const sel = document.getElementById('charaSeriesSelect');
currentCharaSeries = sel.value;
const countEl = document.getElementById('charaSeriesCount');
if (currentCharaSeries && CHARA_DATA[currentCharaSeries]) {
countEl.textContent = CHARA_DATA[currentCharaSeries].length + '件';
} else {
countEl.textContent = '';
}
switchPreset('_chara');
}
function initCharaDropdown() {
const sel = document.getElementById('charaSeriesSelect');
if (!sel) return;
const entries = Object.entries(CHARA_SERIES_NAMES).sort(([a],[b]) => {
const ra = SERIES_READING[a] || a;
const rb = SERIES_READING[b] || b;
return ra.localeCompare(rb, 'ja');
});
entries.forEach(([key, name]) => {
const count = (CHARA_DATA[key] || []).length;
const opt = document.createElement('option');
opt.value = key;
opt.textContent = `${name} (${count})`;
sel.appendChild(opt);
});
sel.options[0].textContent = `-- 作品を選択 (${entries.length}作品 / あいうえお順) --`;
}
function getDisplayTags() {
if (currentPreset === '_search') {
const q = searchQuery.toLowerCase();
const presetTags = [...new Set(Object.values(PRESETS).flat())].filter(t => t.toLowerCase().includes(q));
const charaTags = currentCharaSeries
? (CHARA_DATA[currentCharaSeries] || []).filter(t => t.toLowerCase().includes(q))
: Object.values(CHARA_DATA).flat().filter(t => t.toLowerCase().includes(q));
return [...presetTags, ...charaTags.map(c => c + ', ' + (currentCharaSeries || ''))];
}
if (currentPreset === '_chara' || currentPreset === 'chara') {
let tags = CHARA_DATA[currentCharaSeries] || [];
if (charaSearchQuery) tags = tags.filter(t => t.toLowerCase().includes(charaSearchQuery));
return tags;
}
return PRESETS[currentPreset] || [];
}
function getTagPreset(tag) {
for (const [k, tags] of Object.entries(PRESETS)) { if (tags.includes(tag)) return k; }
return 'general';
}
function renderTags() {
const q = searchQuery.toLowerCase();
const grid = document.getElementById('tagsGrid');
const displayTags = getDisplayTags();
const isChara = currentPreset === '_chara' || currentPreset === 'chara';
const arr = isNegPreset(currentPreset) ? negSelectedTags : selectedTags;
const selNames = arr.map(s => s.tag);
grid.innerHTML = displayTags.map((t, i) => {
const fullTag = isChara ? `${t}, ${currentCharaSeries}` : t;
const isSel = selNames.includes(fullTag);
const isMatch = q && t.toLowerCase().includes(q);
const jp = TAG_JP[t] ? `<span class="tag-jp">${TAG_JP[t]}</span>` : '';
return `<span class="tag${isSel?' selected':''}${isMatch&&!isSel?' search-match':''}">${t}${jp}</span>`;
}).join('');
grid.querySelectorAll('.tag').forEach((el, i) => { el.onclick = () => toggleTag(displayTags[i]); });
}
function isAllSelected() {
const tags = getDisplayTags();
const isChara = currentPreset === '_chara' || currentPreset === 'chara';
const arr = isNegPreset(currentPreset) ? negSelectedTags : selectedTags;
const selNames = arr.map(s => s.tag);
return tags.length > 0 && tags.every(t => selNames.includes(isChara ? `${t}, ${currentCharaSeries}` : t));
}
function updateAllBtn() {
const btn = document.getElementById('allBtn'); const all = isAllSelected();
btn.textContent = all ? 'ALL OFF' : 'ALL ON'; btn.classList.toggle('all-on', all);
}
function toggleSelectAll() {
const isChara = currentPreset === '_chara' || currentPreset === 'chara';
const tags = getDisplayTags();
const isNeg = isNegPreset(currentPreset);
const arr = isNeg ? negSelectedTags : selectedTags;
const selNames = arr.map(s => s.tag);
if (isAllSelected()) {
const fullTags = tags.map(t => isChara ? `${t}, ${currentCharaSeries}` : t);
if (isNeg) negSelectedTags = negSelectedTags.filter(s => !fullTags.includes(s.tag));
else selectedTags = selectedTags.filter(s => !fullTags.includes(s.tag));
} else {
const storeP = isChara ? 'chara' : currentPreset;
tags.forEach(t => {
const fullT = isChara ? `${t}, ${currentCharaSeries}` : t;
if(!selNames.includes(fullT)) arr.push({tag: fullT, preset: storeP});
});
}
renderTags();
if (isNeg) renderNegSelZone(); else renderSelZone();
updateAllBtn(); updateTagCount(); updateFinal();
}
function isNegPreset(key) { return key === 'neg_animagine' || key === 'neg_pony' || key === 'photo_neg' || key === 'neg_ani_skin'; }
// ANI/PONYの行ごとに全オン/オフ(ポジ+ネガ両方まとめて)
function toggleSelectAllPreset(posKey, negKey, btnId) {
const posTags = PRESETS[posKey] || [];
const negTags = PRESETS[negKey] || [];
const posSelNames = selectedTags.map(s => s.tag);
const negSelNames = negSelectedTags.map(s => s.tag);
const allOn = posTags.every(t => posSelNames.includes(t)) && negTags.every(t => negSelNames.includes(t));
if (allOn) {
// 全オフ
selectedTags = selectedTags.filter(s => !posTags.includes(s.tag));
negSelectedTags = negSelectedTags.filter(s => !negTags.includes(s.tag));
} else {
// 全オン
posTags.forEach(t => { if (!posSelNames.includes(t)) selectedTags.push({tag:t, preset:posKey}); });
negTags.forEach(t => { if (!negSelNames.includes(t)) negSelectedTags.push({tag:t, preset:negKey}); });
}
// ボタン表示更新
const btn = document.getElementById(btnId);
const nowAllOn = posTags.every(t => selectedTags.some(s=>s.tag===t)) && negTags.every(t => negSelectedTags.some(s=>s.tag===t));
btn.textContent = nowAllOn ? 'ALL OFF' : 'ALL ON';
btn.classList.toggle('all-on', nowAllOn);
renderTags(); renderSelZone(); renderNegSelZone(); updateAllBtn(); updateTagCount(); updateFinal();
}
function toggleTag(tag) {
const isChara = currentPreset === '_chara' || currentPreset === 'chara';
const fullTag = isChara ? `${tag}, ${currentCharaSeries}` : tag;
const storePreset = isChara ? 'chara' : currentPreset;
if (isNegPreset(currentPreset)) {
const idx = negSelectedTags.findIndex(s => s.tag === fullTag);
if (idx > -1) negSelectedTags.splice(idx, 1);
else negSelectedTags.push({tag: fullTag, preset: storePreset});
renderTags(); renderNegSelZone(); updateAllBtn(); updateTagCount(); updateFinal();
} else {
const idx = selectedTags.findIndex(s => s.tag === fullTag);
if (idx > -1) selectedTags.splice(idx, 1);
else selectedTags.push({tag: fullTag, preset: storePreset});
renderTags(); renderSelZone(); updateAllBtn(); updateTagCount(); updateFinal();
}
}
function updateTagCount() {
const total = selectedTags.length + negSelectedTags.length;
document.getElementById('tagCount').textContent = `${total} selected`;
}
/* ===== NEG SEL ZONE ===== */
function renderNegSelZone() {
const zone = document.getElementById('negSelZone');
if (!negSelectedTags.length) {
zone.innerHTML = '<span class="empty-zone">ネガティブタグをクリックして追加</span>';
return;
}
let html = '';
negSelectedTags.forEach((s, i) => {
const meta = PRESET_META[s.preset] || PRESET_META.neg_animagine;
html += `<span class="sel-tag ${meta.colorClass}" draggable="true" data-neg-idx="${i}"
ondragstart="negDragStart(event,${i})" ondragover="negDragOver(event,${i})"
ondrop="negDragDrop(event,${i})" ondragend="negDragEnd()">
${escHtml(s.tag)}<span class="del-tag" onclick="removeNegSelTag(${i})">×</span>
</span>`;
});
zone.innerHTML = html;
}
let negDragSrcIdx = null;
function negDragStart(e,i) { negDragSrcIdx=i; e.currentTarget.classList.add('dragging'); }
function negDragOver(e,i) {
e.preventDefault();
document.querySelectorAll('#negSelZone .sel-tag').forEach(el => {
el.classList.toggle('drag-over', parseInt(el.dataset.negIdx)===i && i!==negDragSrcIdx);
});
}
function negDragDrop(e,i) {
e.preventDefault();
if(negDragSrcIdx===null||negDragSrcIdx===i)return;
const item=negSelectedTags.splice(negDragSrcIdx,1)[0];
const newIdx = negDragSrcIdx < i ? i - 1 : i;
negSelectedTags.splice(newIdx,0,item);
negDragSrcIdx=null;
renderNegSelZone(); updateFinal();
}
function negDragEnd() { negDragSrcIdx=null; document.querySelectorAll('#negSelZone .sel-tag').forEach(el=>el.classList.remove('dragging','drag-over')); }
function removeNegSelTag(i) {
negSelectedTags.splice(i, 1);
renderTags(); renderNegSelZone(); updateAllBtn(); updateTagCount(); updateFinal();
}
/* ===== SEL ZONE — shows translated text + grouped color tags ===== */
function initGroupOrder() {
if (!groupOrder.length && selectedTags.length) {
const seen = new Set();
[...selectedTags].sort((a,b) => {
const pa = (PRESET_META[a.preset]||PRESET_META.general).priority;
const pb = (PRESET_META[b.preset]||PRESET_META.general).priority;
return pa - pb;
}).forEach(s => {
const g = (PRESET_META[s.preset]||PRESET_META.general).group;
if (!seen.has(g)) { seen.add(g); groupOrder.push(g); }
});
}
}
function getSortedSelTags() {
if (!groupOrder.length) {
return [...selectedTags].sort((a, b) => {
const pa = (PRESET_META[a.preset] || PRESET_META.general).priority;
const pb = (PRESET_META[b.preset] || PRESET_META.general).priority;
return pa - pb;
});
}
return [...selectedTags].sort((a, b) => {
const ga = (PRESET_META[a.preset]||PRESET_META.general).group;
const gb = (PRESET_META[b.preset]||PRESET_META.general).group;
let ia = groupOrder.indexOf(ga); if (ia < 0) ia = groupOrder.length;
let ib = groupOrder.indexOf(gb); if (ib < 0) ib = groupOrder.length;
if (ia !== ib) return ia - ib;
return selectedTags.indexOf(a) - selectedTags.indexOf(b);
});
}
function renderSelZone() {
const zone = document.getElementById('selZone');
let html = '';
// ── 翻訳テキスト表示 ──
if (translatedText) {
html += `<div style="width:100%;flex-basis:100%;">
<span style="font-family:'Orbitron',monospace;font-size:7px;letter-spacing:1px;color:var(--accent3);opacity:.7;">TRANSLATED ▸</span>
<div style="margin-top:3px;padding:6px 9px;background:rgba(0,229,255,.06);border:1px dashed rgba(0,229,255,.2);border-radius:6px;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--accent3);line-height:1.7;word-break:break-all;">${escHtml(translatedText)}</div>
</div>`;
}
if (!selectedTags.length) {
if (!translatedText) html += '<span class="empty-zone">タグをクリックして追加</span>';
zone.innerHTML = html; return;
}
const sorted = getSortedSelTags();
let lastGroup = null;
// グループ一覧を収集(表示順)
const groups = [];
sorted.forEach(s => {
const g = (PRESET_META[s.preset] || PRESET_META.general).group;
if (!groups.length || groups[groups.length-1] !== g) groups.push(g);
});
sorted.forEach((s, dispIdx) => {
const meta = PRESET_META[s.preset] || PRESET_META.general;
const g = meta.group;
const glKey = (g === 'ani' || g === 'pony') ? s.preset : g === 'neg' ? s.preset : g;
if (g !== lastGroup) {
if (lastGroup !== null) html += '<div class="zone-sep"></div>';
const gi = groups.indexOf(g);
const canUp = gi > 0;
const canDown = gi < groups.length - 1;
html += `<span class="zone-group-label zgl-${glKey}" style="display:inline-flex;align-items:center;gap:2px;">
${meta.label}
${canUp ? `<span class="grp-move-btn" onclick="moveGroupUp('${g}')" title="このグループを上へ" style="cursor:pointer;font-size:9px;opacity:.6;padding:0 2px;">▲</span>` : ''}
${canDown ? `<span class="grp-move-btn" onclick="moveGroupDown('${g}')" title="このグループを下へ" style="cursor:pointer;font-size:9px;opacity:.6;padding:0 2px;">▼</span>` : ''}
</span>`;
lastGroup = g;
}
const origIdx = selectedTags.indexOf(s);
const isNsfw = meta.group === 'nsfw';
const isLocked = lockedNsfwTags.has(s.tag);
const lockBtn = isNsfw
? `<span class="lock-btn${isLocked?' is-locked':''}" onclick="toggleLock('${escHtml(s.tag)}')" title="${isLocked?'ロック中(入れ替え対象外)':'クリックでロック(入れ替えしない)'}">${isLocked?'🔒':'🔓'}</span>`
: '';
html += `<span class="sel-tag ${meta.colorClass}${isLocked?' locked':''}" draggable="true"
data-disp="${dispIdx}"
ondragstart="dragStart(event,${dispIdx})" ondragover="dragOver(event,${dispIdx})"
ondrop="dragDrop(event,${dispIdx})" ondragend="dragEnd()">
${escHtml(s.tag)}${lockBtn}<span class="del-tag" onclick="removeSelTag(${origIdx})">×</span>
</span>`;
});
zone.innerHTML = html;
}
function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
function toggleLock(tag) { if(lockedNsfwTags.has(tag)) lockedNsfwTags.delete(tag); else lockedNsfwTags.add(tag); renderSelZone(); }
function removeSelTag(i) { lockedNsfwTags.delete(selectedTags[i]?.tag); selectedTags.splice(i,1); renderTags(); renderSelZone(); updateAllBtn(); updateTagCount(); updateFinal(); }
// グループ一括移動
function moveGroupUp(group) {
initGroupOrder();
const gi = groupOrder.indexOf(group);
if (gi <= 0) return;
[groupOrder[gi-1], groupOrder[gi]] = [groupOrder[gi], groupOrder[gi-1]];
renderSelZone(); updateFinal();
}
function moveGroupDown(group) {
initGroupOrder();
const gi = groupOrder.indexOf(group);
if (gi < 0 || gi >= groupOrder.length - 1) return;
[groupOrder[gi], groupOrder[gi+1]] = [groupOrder[gi+1], groupOrder[gi]];
renderSelZone(); updateFinal();
}
// ドラッグ&ドロップ(表示インデックスで操作)
function dragStart(e, dispIdx) {
dragSrcIdx = dispIdx;
e.currentTarget.classList.add('dragging');
}
function dragOver(e, dispIdx) {
e.preventDefault();
document.querySelectorAll('#selZone .sel-tag').forEach(el => {
el.classList.toggle('drag-over', parseInt(el.dataset.disp) === dispIdx && dispIdx !== dragSrcIdx);
});
}
function dragDrop(e, dropDispIdx) {
e.preventDefault();
if (dragSrcIdx === null || dragSrcIdx === dropDispIdx) return;
// 現在の表示順で並び替え
const sorted = getSortedSelTags();
const item = sorted.splice(dragSrcIdx, 1)[0];
const newIdx = dragSrcIdx < dropDispIdx ? dropDispIdx - 1 : dropDispIdx;
sorted.splice(newIdx, 0, item);
// selectedTags を新しい順に再構築
selectedTags = sorted;
// groupOrder を新しい順に更新
const seen = new Set();
sorted.forEach(s => {
const g = (PRESET_META[s.preset]||PRESET_META.general).group;
if (!seen.has(g)) { seen.add(g); }
});
groupOrder = [];
const seenSet2 = new Set();
sorted.forEach(s => {
const g = (PRESET_META[s.preset]||PRESET_META.general).group;
if (!seenSet2.has(g)) { seenSet2.add(g); groupOrder.push(g); }
});
dragSrcIdx = null;
renderSelZone(); renderTags(); updateFinal();
}
function dragEnd() {
dragSrcIdx = null;
document.querySelectorAll('.sel-tag').forEach(el => el.classList.remove('dragging','drag-over'));
}
/* ===== RANDOM ===== */
// 現在のNSFWタブから1つ
function randomOne() {
if (!currentPreset.startsWith('nsfw_')) return;
const tags = getDisplayTags();
const selNames = selectedTags.map(s => s.tag);
const candidates = tags.filter(t => !selNames.includes(t));
const res = document.getElementById('randomResult');
if (!candidates.length) {
res.style.display=''; res.textContent='✓ このタブのタグはすべて選択済みです';
setTimeout(()=>res.style.display='none', 2500); return;
}
const pick = candidates[Math.floor(Math.random()*candidates.length)];
selectedTags.push({tag:pick, preset:currentPreset});
const jp = TAG_JP[pick] ? `${TAG_JP[pick]}` : '';
res.style.display=''; res.textContent=`🎲 選択: ${pick}${jp}`;
setTimeout(()=>res.style.display='none', 3000);
renderTags(); renderSelZone(); updateTagCount(); updateFinal();
}
// NSFWタブ全て一括 — 各タブから1つずつ
// NSFWタブ全て一括 — 2回目押しで前回分を削除し新たに1つずつ選択
function randomAllNsfw() {
const btn = document.getElementById('nsfwAllBtn');
if (nsfwAllPicked.length) {
// ロックされていない前回分だけ削除
const toRemove = nsfwAllPicked.filter(t => !lockedNsfwTags.has(t));
selectedTags = selectedTags.filter(s => !toRemove.includes(s.tag));
nsfwAllPicked = nsfwAllPicked.filter(t => lockedNsfwTags.has(t)); // ロック分は保持
btn.textContent = '🎲 全タブ一括';
btn.style.borderColor = '';
btn.style.color = '';
renderTags(); renderSelZone(); updateTagCount(); updateFinal();
}
// ロックされていないタブから新たに1つずつ選ぶ
const selNames = selectedTags.map(s => s.tag);
const added = [];
NSFW_TABS.forEach(tabKey => {
// このタブにロック済みタグがあればスキップ
const alreadyLockedInTab = (PRESETS[tabKey] || []).some(t => lockedNsfwTags.has(t));
if (alreadyLockedInTab) return;
const tags = PRESETS[tabKey] || [];
const cands = tags.filter(t => !selNames.includes(t) && !added.includes(t));
if (!cands.length) return;
const pick = cands[Math.floor(Math.random() * cands.length)];
selectedTags.push({tag: pick, preset: tabKey});
added.push(pick);
selNames.push(pick);
});
nsfwAllPicked = [...nsfwAllPicked, ...added]; // ロック分+新規
const res = document.getElementById('randomResult');
const lockedCount = lockedNsfwTags.size;
if (added.length) {
btn.textContent = '🔄 入れ替え';
btn.style.borderColor = '#ffab40';
btn.style.color = '#ffab40';
res.style.display = '';
const lockMsg = lockedCount ? ` 🔒${lockedCount}件ロック中` : '';
res.textContent = `🎲 ${added.length}件追加${lockMsg}: ${added.slice(0, 5).join(', ')}${added.length > 5 ? '…' : ''}`;
setTimeout(() => res.style.display = 'none', 4000);
}
renderTags(); renderSelZone(); updateTagCount(); updateFinal();
}
/* ===== TRANSLATION ===== */
async function doTranslate(type) {
if (!apiKey||!apiKey.startsWith('sk-ant')) { alert('APIキーを入力してくださいページ上部'); return; }
const isPos = type==='pos';
const jp = document.getElementById(isPos?'jpInput':'jpNegInput').value.trim();
if (!jp) return;
const btn = document.getElementById(isPos?'translateBtn':'translateNegBtn');
const out = document.getElementById(isPos?'translatedOutput':'translatedNegOutput');
btn.disabled=true; btn.innerHTML='<span class="spinner"></span>翻訳中...';
out.className=`output-area${isPos?'':' neg-color'}`; out.textContent='翻訳しています...';
const sys = isPos
? `You are an expert at converting Japanese image generation prompts to English for Stable Diffusion / Fooocus.\nOutput ONLY comma-separated English tags. No explanations, no Japanese, no preamble.`
: `You are an expert at converting Japanese NEGATIVE image generation prompts to English for Stable Diffusion / Fooocus.\nOutput ONLY comma-separated English negative tags. No explanations, no Japanese, no preamble.`;
try {
const r = await fetch('https://api.anthropic.com/v1/messages',{method:'POST',
headers:{'Content-Type':'application/json','x-api-key':apiKey,'anthropic-version':'2023-06-01','anthropic-dangerous-direct-browser-access':'true'},
body:JSON.stringify({model:'claude-haiku-4-5-20251001',max_tokens:500,system:sys,messages:[{role:'user',content:`Translate:\n${jp}`}]})});
const data = await r.json();
if (data.error) throw new Error(data.error.message);
const result = data.content?.[0]?.text?.trim()||'エラー';
if (isPos) { translatedText=result; out.textContent=result; out.className='output-area'; addHistory(jp,result,'pos'); }
else { translatedNegText=result; out.textContent=result; out.className='output-area neg-color'; addHistory(jp,result,'neg'); }
updateFinal(); renderSelZone();
} catch(e) { out.textContent='エラー: '+e.message; out.className='output-area empty'; }
btn.disabled=false; btn.innerHTML=isPos?'✦ 英語に翻訳する':'✦ ネガティブを翻訳する';
}
/* ===== FINAL ===== */
function getFinalPos() {
const sorted = getSortedSelTags();
const parts=[];
if(translatedText) parts.push(translatedText);
if(sorted.length) parts.push(sorted.map(s=>s.tag).join(', '));
return parts.join(', ');
}
function getFinalNeg() {
const parts=[];
if(translatedNegText) parts.push(translatedNegText);
if(negSelectedTags.length) parts.push(negSelectedTags.map(s=>s.tag).join(', '));
return parts.join(', ');
}
function appendToFinal() { updateFinal(); }
function updateFinal() {
document.getElementById('finalOutput').textContent = getFinalPos()||'ここにポジティブプロンプトが表示されます';
document.getElementById('finalNegOutput').textContent = getFinalNeg()||'ここにネガティブプロンプトが表示されます';
if (typeof window._pfUpdateEmbedPreview === 'function') window._pfUpdateEmbedPreview();
}
/* ===== SAVE ===== */
function onImageSelect(e) {
const file = e.target.files[0]; if(!file) return;
const reader = new FileReader();
reader.onload = ev => {
pendingImageData = ev.target.result;
const thumb = document.getElementById('saveImgThumb');
thumb.src = pendingImageData;
document.getElementById('saveImagePreview').style.display='';
};
reader.readAsDataURL(file);
}
function clearImageInput() {
pendingImageData=null;
document.getElementById('saveImageInput').value='';
document.getElementById('saveImagePreview').style.display='none';
document.getElementById('saveImgThumb').src='';
}
// 画像リサイズonload完全待ち・安全版
function resizeImage(dataUrl, maxW) {
return new Promise(resolve => {
const img = new Image();
img.onload = () => {
try {
const w = img.naturalWidth || img.width;
const h = img.naturalHeight || img.height;
if (!w || !h) { resolve(dataUrl); return; }
const scale = Math.min(1, maxW / w);
const cw = Math.max(1, Math.round(w * scale));
const ch = Math.max(1, Math.round(h * scale));
const canvas = document.createElement('canvas');
canvas.width = cw; canvas.height = ch;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#ffffff'; // 背景白透過PNG対策
ctx.fillRect(0, 0, cw, ch);
ctx.drawImage(img, 0, 0, cw, ch);
const result = canvas.toDataURL('image/jpeg', 0.82);
// canvas が黒になった場合(空データ)はオリジナルを返す
if (result === 'data:,') { resolve(dataUrl); return; }
resolve(result);
} catch(e) { resolve(dataUrl); }
};
img.onerror = () => resolve(dataUrl);
img.src = dataUrl;
});
}
async function savePrompt() {
let name = document.getElementById('saveNameInput').value.trim();
// 名前未入力なら日時を自動設定
if (!name) {
const now = new Date();
name = `保存_${now.getMonth()+1}/${now.getDate()}_${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
}
const pos = getFinalPos();
const neg = getFinalNeg();
console.log('[savePrompt] pos:', pos ? pos.slice(0,60) : '(empty)');
console.log('[savePrompt] neg:', neg ? neg.slice(0,60) : '(empty)');
if (!pos && !neg) { showSaveMsg('⚠ 保存するプロンプトがありません。タグを選ぶか翻訳してください。', 'warn'); return; }
const now = new Date();
const time = `${now.getMonth()+1}/${now.getDate()} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
let imgData = null;
try { imgData = pendingImageData ? await resizeImage(pendingImageData, 300) : null; } catch(e) { console.warn('[savePrompt] resizeImage error:', e); }
saves.unshift({name, pos, neg, time, img: imgData, tags: selectedTags.map(t=>({...t})), negTags: negSelectedTags.map(t=>({...t}))});
document.getElementById('saveNameInput').value = '';
clearImageInput();
try {
localStorage.setItem('pf_saves', JSON.stringify(saves));
console.log('[savePrompt] saved OK, count:', saves.length);
renderSaves();
showSaveMsg('💾 保存しました:' + name, 'ok');
} catch(e) {
console.warn('[savePrompt] localStorage error, retrying without img:', e);
try {
const savesNoImg = saves.map(s => ({...s, img: null}));
localStorage.setItem('pf_saves', JSON.stringify(savesNoImg));
renderSaves();
showSaveMsg('💾 保存しました(画像は容量超過のため除外)', 'ok');
} catch(e2) {
console.error('[savePrompt] save failed:', e2);
showSaveMsg('⚠ 保存に失敗しました(ストレージ容量不足)', 'warn');
}
}
}
function showSaveMsg(msg, type) {
let el = document.getElementById('saveToast');
if (!el) {
el = document.createElement('div');
el.id = 'saveToast';
el.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);z-index:9999;padding:9px 22px;border-radius:20px;font-family:"JetBrains Mono",monospace;font-size:11px;pointer-events:none;transition:opacity .4s;white-space:nowrap;box-shadow:0 4px 20px rgba(0,0,0,.5);';
document.body.appendChild(el);
}
el.textContent = msg;
el.style.opacity = '1';
el.style.background = type === 'ok' ? 'rgba(0,35,25,.97)' : 'rgba(40,10,10,.97)';
el.style.border = type === 'ok' ? '1px solid rgba(0,229,160,.55)' : '1px solid rgba(255,107,107,.55)';
el.style.color = type === 'ok' ? '#00e5a0' : '#ff7070';
clearTimeout(el._timer);
el._timer = setTimeout(() => { el.style.opacity = '0'; }, 3000);
}
// 保存済みアイテムに画像を後から追加
function openAddImg(idx) {
addImgTargetIdx = idx;
document.getElementById('addImgInput').click();
}
function onAddImgSelect(e) {
const file = e.target.files[0]; if(!file||addImgTargetIdx===null) return;
const reader = new FileReader();
reader.onload = async ev => {
const resized = await resizeImage(ev.target.result, 300);
saves[addImgTargetIdx].img = resized;
try{ localStorage.setItem('pf_saves',JSON.stringify(saves)); }catch(e){}
renderSaves();
addImgTargetIdx=null;
e.target.value='';
};
reader.readAsDataURL(file);
}
function renderSaves() {
const list = document.getElementById('saveList');
if (!saves.length) { list.innerHTML='<div class="empty-saves">保存なし</div>'; return; }
list.innerHTML = saves.map((s,i) => {
const imgBlock = s.img
? `<img src="${s.img}" style="width:60px;height:60px;object-fit:cover;border-radius:6px;border:1px solid var(--border);flex-shrink:0;cursor:pointer;" onclick="expandImg(${i})" title="クリックで拡大">`
: `<div onclick="openAddImg(${i})" title="画像を追加" style="width:60px;height:60px;border-radius:6px;border:1px dashed var(--border);flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:20px;cursor:pointer;color:var(--text2);transition:border-color .2s;" onmouseenter="this.style.borderColor='var(--accent3)'" onmouseleave="this.style.borderColor='var(--border)'">🖼️</div>`;
return `<div class="save-item" style="display:flex;gap:8px;align-items:flex-start;">
${imgBlock}
<div style="flex:1;min-width:0;">
<div class="save-item-name">${s.name} <span style="color:var(--text2);font-size:8px;">${s.time}</span></div>
${s.pos?`<div class="save-item-pos">${s.pos.slice(0,70)}${s.pos.length>70?'...':''}</div>`:''}
${s.neg?`<div class="save-item-neg">[-] ${s.neg.slice(0,50)}${s.neg.length>50?'...':''}</div>`:''}
<div class="save-item-row">
<button class="btn" style="font-size:8px;" onclick="loadSave(${i})">↑ 読み込む</button>
<button class="btn btn-save" style="font-size:8px;" onclick="quickOverwrite(${i})" title="現在のプロンプトでこの保存を上書き">♻ 上書き</button>
${s.img?`<button class="btn btn-save" style="font-size:8px;" onclick="openAddImg(${i})">🖼 差替</button>`:''}
<button class="btn btn-d" style="font-size:8px;" onclick="deleteSave(${i})">削除</button>
</div>
</div>
</div>`;
}).join('');
}
function expandImg(i) {
const s=saves[i]; if(!s||!s.img) return;
const ov=document.createElement('div');
ov.style.cssText='position:fixed;inset:0;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;z-index:9999;cursor:pointer;';
ov.innerHTML=`<img src="${s.img}" style="max-width:90vw;max-height:90vh;border-radius:10px;box-shadow:0 0 40px rgba(0,0,0,.8);">`;
ov.onclick=()=>document.body.removeChild(ov);
document.body.appendChild(ov);
}
async function quickOverwrite(i) {
const pos = getFinalPos(); const neg = getFinalNeg();
if (!pos && !neg) { showSaveMsg('⚠ 保存するプロンプトがありません', 'warn'); return; }
const now = new Date();
const time = `${now.getMonth()+1}/${now.getDate()} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
saves[i] = {...saves[i], pos, neg, time};
try { localStorage.setItem('pf_saves', JSON.stringify(saves)); } catch(e) {}
renderSaves();
showSaveMsg('♻ 上書き保存しました:' + saves[i].name, 'ok');
}
function loadSave(i) {
const s=saves[i];
if(s.pos){translatedText=s.pos;document.getElementById('translatedOutput').textContent=s.pos;document.getElementById('translatedOutput').className='output-area';}
if(s.neg){translatedNegText=s.neg;document.getElementById('translatedNegOutput').textContent=s.neg;document.getElementById('translatedNegOutput').className='output-area neg-color';}
if(s.tags){selectedTags=s.tags.map(t=>({...t}));}
if(s.negTags){negSelectedTags=s.negTags.map(t=>({...t}));}
groupOrder=[];
renderTags(); renderSelZone(); renderNegSelZone(); updateAllBtn(); updateTagCount(); updateFinal();
}
function deleteSave(i) { saves.splice(i,1); try{ localStorage.setItem('pf_saves',JSON.stringify(saves)); }catch(e){} renderSaves(); }
async function overwriteSave() {
let name = document.getElementById('saveNameInput').value.trim();
if (!name) { showSaveMsg('⚠ 上書きする保存名を入力してください', 'warn'); return; }
const idx = saves.findIndex(s => s.name === name);
if (idx === -1) { showSaveMsg('⚠ 同名の保存が見つかりません。新規保存してください', 'warn'); return; }
const pos = getFinalPos();
const neg = getFinalNeg();
if (!pos && !neg) { showSaveMsg('⚠ 保存するプロンプトがありません', 'warn'); return; }
const now = new Date();
const time = `${now.getMonth()+1}/${now.getDate()} ${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
let imgData = saves[idx].img;
try { if (pendingImageData) imgData = await resizeImage(pendingImageData, 300); } catch(e) {}
saves[idx] = {...saves[idx], pos, neg, time, img: imgData};
clearImageInput();
try {
localStorage.setItem('pf_saves', JSON.stringify(saves));
renderSaves();
showSaveMsg('♻ 上書き保存しました:' + name, 'ok');
} catch(e) {
try {
const savesNoImg = saves.map(s => ({...s, img: null}));
localStorage.setItem('pf_saves', JSON.stringify(savesNoImg));
renderSaves();
showSaveMsg('♻ 上書き保存しました(画像除外)', 'ok');
} catch(e2) { showSaveMsg('⚠ 保存に失敗しました', 'warn'); }
}
}
function downloadSaves() {
if (!saves.length) { showSaveMsg('⚠ 保存データがありません', 'warn'); return; }
const json = JSON.stringify(saves, null, 2);
const blob = new Blob([json], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const d = new Date();
a.download = `prompt_forge_saves_${d.getFullYear()}${String(d.getMonth()+1).padStart(2,'0')}${String(d.getDate()).padStart(2,'0')}.json`;
a.click();
URL.revokeObjectURL(url);
showSaveMsg('⬇ ダウンロードしました', 'ok');
}
function importSaves(e) {
const file = e.target.files[0]; if (!file) return;
const reader = new FileReader();
reader.onload = ev => {
try {
const data = JSON.parse(ev.target.result);
if (!Array.isArray(data)) throw new Error('invalid format');
const merged = [...data];
saves.forEach(s => { if (!merged.find(m => m.name === s.name && m.time === s.time)) merged.push(s); });
saves = merged;
localStorage.setItem('pf_saves', JSON.stringify(saves));
renderSaves();
showSaveMsg(`⬆ インポート完了(${data.length}件)`, 'ok');
} catch(err) {
showSaveMsg('⚠ インポート失敗JSONが正しくありません', 'warn');
}
e.target.value = '';
};
reader.readAsText(file);
}
/* ===== HISTORY ===== */
function addHistory(jp,en,type) {
const now=new Date();
const time=`${now.getHours().toString().padStart(2,'0')}:${now.getMinutes().toString().padStart(2,'0')}`;
history.unshift({jp,en,type,time}); if(history.length>20) history.pop(); renderHistory();
}
function renderHistory() {
const list=document.getElementById('historyList');
if(!history.length){list.innerHTML='<div class="empty-saves">まだ履歴がありません</div>';return;}
list.innerHTML=history.map((h,i)=>`
<div class="save-item" onclick="restoreHist(${i})" style="cursor:pointer;">
<div class="save-item-name">${h.time} · ${h.type==='pos'?'POSITIVE':'NEGATIVE'}</div>
<div style="color:var(--text2);font-size:10px;font-family:'Noto Sans JP',sans-serif;margin-bottom:2px;">${h.jp.slice(0,40)}${h.jp.length>40?'...':''}</div>
<div class="${h.type==='pos'?'save-item-pos':'save-item-neg'}">${h.en.slice(0,70)}${h.en.length>70?'...':''}</div>
</div>`).join('');
}
function restoreHist(i) {
const h=history[i];
if(h.type==='pos'){translatedText=h.en;document.getElementById('translatedOutput').textContent=h.en;document.getElementById('translatedOutput').className='output-area';document.getElementById('jpInput').value=h.jp;}
else{translatedNegText=h.en;document.getElementById('translatedNegOutput').textContent=h.en;document.getElementById('translatedNegOutput').className='output-area neg-color';document.getElementById('jpNegInput').value=h.jp;}
updateFinal(); renderSelZone();
}
function clearHistory() { history=[]; renderHistory(); }
/* ===== UTILS ===== */
function copyBtn(text,id,label) {
if(!text) return;
const btn=document.getElementById(id);
const done=()=>{ btn.textContent='✓ OK'; btn.classList.add('copied'); setTimeout(()=>{ btn.textContent=label; btn.classList.remove('copied'); },1800); };
if(navigator.clipboard && location.protocol !== 'file:') {
navigator.clipboard.writeText(text).then(done).catch(()=>fallbackCopy(text,done));
} else {
fallbackCopy(text,done);
}
}
function fallbackCopy(text,cb) {
const ta=document.createElement('textarea');
ta.value=text; ta.style.cssText='position:fixed;opacity:0;top:0;left:0;';
document.body.appendChild(ta); ta.focus(); ta.select();
try{ document.execCommand('copy'); if(cb)cb(); }catch(e){}
document.body.removeChild(ta);
}
function clearTags() { selectedTags=[]; negSelectedTags=[]; lockedNsfwTags.clear(); nsfwAllPicked=[]; groupOrder=[]; renderTags(); renderSelZone(); renderNegSelZone(); updateAllBtn(); updateTagCount(); updateFinal(); }
function resetAll() {
translatedText=''; translatedNegText=''; selectedTags=[]; negSelectedTags=[]; lockedNsfwTags.clear(); nsfwAllPicked=[]; groupOrder=[];
['jpInput','jpNegInput'].forEach(id=>document.getElementById(id).value='');
document.getElementById('translatedOutput').textContent='← 上で翻訳ボタンを押してください';
document.getElementById('translatedOutput').className='output-area empty';
document.getElementById('translatedNegOutput').textContent='← ネガティブを翻訳してください';
document.getElementById('translatedNegOutput').className='output-area neg-color empty';
clearSearch(); clearImageInput(); renderTags(); renderSelZone(); renderNegSelZone(); updateAllBtn(); updateTagCount(); updateFinal();
}
function toggleColl(el,bodyId) {
const body=document.getElementById(bodyId); const isOpen=body.classList.contains('open');
body.classList.toggle('open',!isOpen); body.classList.toggle('closed',isOpen); el.classList.toggle('open',!isOpen);
}
function switchPage(n) {
// page5に切り替えたとき、最終プロンプトをgenInputに自動反映
[1,2,3,4,5].forEach(function(i){
var c=document.getElementById('page'+i);
var btn=document.getElementById('pageNav'+i);
if(c){ c.classList.toggle('active', i===n); }
if(btn){ btn.classList.toggle('active', i===n); btn.setAttribute('aria-current', i===n ? 'true' : 'false'); }
});
if (n === 5) syncGenFromFinal();
}
let currentSubMode = 'illust';
function switchSubMode(mode) {
currentSubMode = mode;
const p1 = document.getElementById('page1');
p1.classList.toggle('page1-photo', mode === 'photo');
document.getElementById('subModeIllust').classList.toggle('active', mode === 'illust');
document.getElementById('subModePhoto').classList.toggle('active', mode === 'photo');
if (mode === 'photo') {
switchPreset('photo_quality');
} else {
switchPreset('animagine');
}
}
/* ===== MEMO (独立保存) ===== */
const MEMO_LS_KEY = 'pf_memo_entries_v1';
const MEMO_SETTINGS_KEY = 'pf_memo_settings_v1';
let memoEntries = [];
let memoSelectedId = null;
let memoAutoSync = false;
let memoFormTags = [];
let memoTagFilterActive = null;
const MEMO_TAG_PRESETS = ['実写','アニメ','風景','ポートレート','ファンタジー','SF','建築','食べ物','動物','その他'];
function memoNowIso() { return new Date().toISOString(); }
function memoId() { return 'm_' + Math.random().toString(36).slice(2) + Date.now().toString(36); }
function memoLoad() {
try { memoEntries = JSON.parse(localStorage.getItem(MEMO_LS_KEY) || '[]') || []; } catch(e){ memoEntries = []; }
try {
const s = JSON.parse(localStorage.getItem(MEMO_SETTINGS_KEY) || '{}') || {};
memoAutoSync = !!s.autoSync;
const cb = document.getElementById('memoAutoSync');
if (cb) cb.checked = memoAutoSync;
} catch(e){}
memoRenderList();
memoRenderTagPresetsHtml();
// リロード後にフォルダ名ラベルを復元
memoDbGet('settings', 'syncDirHandle').then(h => { if (h) memoUpdateSyncFolderLabel(h.name); }).catch(()=>{});
}
function memoSaveLs() {
localStorage.setItem(MEMO_LS_KEY, JSON.stringify(memoEntries));
}
function memoSetStatus(t) {
const el = document.getElementById('memoStatus');
if (el) el.textContent = t;
}
function memoSetAutoSync() {
memoAutoSync = !!document.getElementById('memoAutoSync')?.checked;
localStorage.setItem(MEMO_SETTINGS_KEY, JSON.stringify({ autoSync: memoAutoSync }));
}
// ---- IndexedDB (images + sync handle) ----
function memoOpenDb() {
return new Promise((resolve, reject) => {
const req = indexedDB.open('pf_memo_db', 1);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains('images')) db.createObjectStore('images');
if (!db.objectStoreNames.contains('settings')) db.createObjectStore('settings');
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function memoDbSet(store, key, value) {
const db = await memoOpenDb();
return await new Promise((resolve, reject) => {
const tx = db.transaction(store, 'readwrite');
tx.objectStore(store).put(value, key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
async function memoDbGet(store, key) {
const db = await memoOpenDb();
return await new Promise((resolve, reject) => {
const tx = db.transaction(store, 'readonly');
const req = tx.objectStore(store).get(key);
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error);
});
}
async function memoDbDel(store, key) {
const db = await memoOpenDb();
return await new Promise((resolve, reject) => {
const tx = db.transaction(store, 'readwrite');
tx.objectStore(store).delete(key);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
}
// ---- Tag helpers ----
function _tagJs(t) { return JSON.stringify(t).replace(/"/g, '&quot;'); }
function memoRenderTagPresetsHtml() {
const el = document.getElementById('memoTagPresets');
if (!el) return;
el.innerHTML = MEMO_TAG_PRESETS.map(t =>
`<button type="button" onclick="memoTagAddFromPreset(${_tagJs(t)})" style="font-size:9px;padding:2px 9px;border:1px solid rgba(0,229,160,.35);border-radius:12px;background:rgba(0,229,160,.06);color:rgba(0,229,160,.8);cursor:pointer;white-space:nowrap;">${escHtml(t)}</button>`
).join('');
}
function memoRenderTagChipsForm() {
const el = document.getElementById('memoTagChipsForm');
if (!el) return;
el.innerHTML = memoFormTags.map(t =>
`<span style="display:inline-flex;align-items:center;gap:3px;background:rgba(0,229,160,.14);border:1px solid rgba(0,229,160,.38);border-radius:12px;padding:1px 7px;font-size:9px;color:rgba(0,229,160,.95);white-space:nowrap;">${escHtml(t)}<span onclick="memoTagRemove(${_tagJs(t)})" style="cursor:pointer;margin-left:1px;opacity:.6;">×</span></span>`
).join('');
}
function memoTagKeydown(ev) {
if (ev.key !== 'Enter' && ev.key !== ',') return;
ev.preventDefault();
const v = ev.target.value.trim().replace(/,/g,'');
if (!v) return;
memoTagAdd(v);
ev.target.value = '';
}
function memoTagAdd(tag) {
tag = tag.trim();
if (!tag || memoFormTags.includes(tag)) return;
memoFormTags.push(tag);
memoRenderTagChipsForm();
}
function memoTagAddFromPreset(tag) { memoTagAdd(tag); }
function memoTagRemove(tag) {
memoFormTags = memoFormTags.filter(t => t !== tag);
memoRenderTagChipsForm();
}
function memoSetFormTags(tags) {
memoFormTags = Array.isArray(tags) ? [...tags] : [];
memoRenderTagChipsForm();
}
function memoRenderTagFilterRow() {
const el = document.getElementById('memoTagFilterRow');
if (!el) return;
const allTags = [...new Set(memoEntries.flatMap(e => e.tags || []))].sort();
if (!allTags.length) { el.innerHTML = ''; return; }
const btn = (label, val) => {
const active = memoTagFilterActive === val;
return `<button type="button" onclick="memoSetTagFilter(${_tagJs(val)})" style="font-size:9px;padding:2px 9px;border:1px solid ${active?'rgba(0,229,160,.85)':'rgba(255,255,255,.2)'};border-radius:12px;background:${active?'rgba(0,229,160,.18)':'rgba(255,255,255,.04)'};color:${active?'rgba(0,229,160,1)':'rgba(255,255,255,.5)'};cursor:pointer;white-space:nowrap;">${escHtml(label)}</button>`;
};
el.innerHTML = btn('すべて', null) + allTags.map(t => btn(t, t)).join('');
}
function memoSetTagFilter(tag) {
memoTagFilterActive = tag;
memoRenderList();
}
function memoGetForm() {
return {
title: (document.getElementById('memoTitle')?.value || '').trim(),
model: (document.getElementById('memoModel')?.value || 'Fooocus/Animagine').trim(),
prompt: (document.getElementById('memoPrompt')?.value || '').trim(),
tags: [...memoFormTags],
};
}
function memoRenderList() {
const q = (document.getElementById('memoSearch')?.value || '').toLowerCase().trim();
const sortVal = document.getElementById('memoSort')?.value || 'updated_desc';
const list = document.getElementById('memoList');
if (!list) return;
memoRenderTagFilterRow();
const _sortFns = {
updated_desc: (a,b) => (b.updatedAt||b.createdAt||'').localeCompare(a.updatedAt||a.createdAt||''),
updated_asc: (a,b) => (a.updatedAt||a.createdAt||'').localeCompare(b.updatedAt||b.createdAt||''),
created_desc: (a,b) => (b.createdAt||'').localeCompare(a.createdAt||''),
title_asc: (a,b) => (a.title||'').localeCompare(b.title||'', 'ja'),
title_desc: (a,b) => (b.title||'').localeCompare(a.title||'', 'ja'),
};
const sortFn = _sortFns[sortVal] || _sortFns.updated_desc;
const filtered = memoEntries
.slice()
.sort(sortFn)
.filter(e => {
if (q && !(e.title||'').toLowerCase().includes(q) && !(e.prompt||'').toLowerCase().includes(q) && !(e.model||'').toLowerCase().includes(q)) return false;
if (memoTagFilterActive !== null && !(e.tags||[]).includes(memoTagFilterActive)) return false;
return true;
});
if (!filtered.length) { list.innerHTML = '<div class="empty-saves">保存なし</div>'; return; }
list.innerHTML = filtered.map(e => {
const active = e.id === memoSelectedId ? ' style="border-color:#00e5a0;"' : '';
const t = escHtml(e.title || '(無題)');
const m = escHtml(e.model || '');
const dt = (e.updatedAt || e.createdAt || '').slice(0,19).replace('T',' ');
const tagsHtml = (e.tags||[]).length
? `<div style="display:flex;flex-wrap:wrap;gap:3px;margin-top:3px;">${(e.tags||[]).map(tg=>`<span style="font-size:8px;padding:1px 6px;border:1px solid rgba(0,229,160,.32);border-radius:10px;color:rgba(0,229,160,.8);background:rgba(0,229,160,.08);">${escHtml(tg)}</span>`).join('')}</div>`
: '';
return `<div class="save-item"${active} onclick="memoSelect('${e.id}')">
<div class="save-item-name">${t}</div>
<div class="save-item-pos" style="color:rgba(255,255,255,.7)">${m} <span style="opacity:.5">|</span> <span style="opacity:.6;font-size:9px;">${dt}</span></div>
<div class="save-item-neg" style="color:rgba(224,224,255,.6)">${escHtml((e.prompt||'').slice(0,120))}${(e.prompt||'').length>120?'…':''}</div>
${tagsHtml}
</div>`;
}).join('');
}
function memoSelect(id) {
memoSelectedId = id;
const e = memoEntries.find(x=>x.id===id);
if (!e) return;
const t = document.getElementById('memoTitle'); if (t) t.value = e.title || '';
const m = document.getElementById('memoModel'); if (m) m.value = e.model || 'Fooocus/Animagine';
const p = document.getElementById('memoPrompt'); if (p) p.value = e.prompt || '';
memoSetFormTags(e.tags || []);
memoSetStatus('選択中: ' + (e.updatedAt || e.createdAt || '').slice(0,19).replace('T',' '));
memoRenderPreview(e);
memoRenderList();
}
async function memoRenderPreview(e) {
const prev = document.getElementById('memoPreview');
if (prev) {
prev.className = 'output-area';
prev.textContent =
'TITLE: ' + (e.title || '(無題)') + '\n' +
'MODEL: ' + (e.model || '') + '\n' +
'TAGS: ' + ((e.tags||[]).join(', ') || '—') + '\n' +
'CREATED: ' + (e.createdAt || '') + '\n' +
'UPDATED: ' + (e.updatedAt || '') + '\n\n' +
(e.prompt || '');
}
const imgWrap = document.getElementById('memoImages');
if (imgWrap) imgWrap.innerHTML = '';
const meta = document.getElementById('memoMeta');
if (meta) { meta.className = 'output-area empty'; meta.textContent = '画像メタデータはここに表示'; }
if (!e.imageIds || !e.imageIds.length) return;
for (const imgId of e.imageIds) {
const rec = await memoDbGet('images', imgId);
if (!rec || !rec.blob) continue;
const url = URL.createObjectURL(rec.blob);
const el = document.createElement('img');
el.src = url;
el.style.cssText = 'height:64px;border-radius:8px;border:1px solid rgba(255,255,255,.12);cursor:pointer;object-fit:cover;';
el.title = rec.name || imgId;
el.onclick = async function() {
const m = document.getElementById('memoMeta');
if (!m) return;
m.className = 'output-area';
m.textContent = '読み込み中...';
const md = rec.meta || {};
m.textContent = JSON.stringify(md, null, 2);
};
imgWrap.appendChild(el);
}
}
function memoCopyPrompt() {
const e = memoEntries.find(x=>x.id===memoSelectedId);
if (!e) return;
copyBtn(e.prompt || '', 'memoCopyBtn', 'COPY PROMPT');
}
function memoCopySummary() {
const e = memoEntries.find(x=>x.id===memoSelectedId);
if (!e) return;
const t =
'TITLE: ' + (e.title||'(無題)') + '\n' +
'MODEL: ' + (e.model||'') + '\n' +
'CREATED: ' + (e.createdAt||'') + '\n' +
'UPDATED: ' + (e.updatedAt||'') + '\n\n' +
(e.prompt||'');
fallbackCopy(t, function(){ memoSetStatus('コピーしました'); });
}
async function memoAddImages(ev) {
const files = Array.from(ev.target?.files || []);
if (!files.length) return;
if (!memoSelectedId) {
memoSetStatus('先に保存(新規保存)してから画像追加してください');
ev.target.value = '';
return;
}
const e = memoEntries.find(x=>x.id===memoSelectedId);
if (!e) return;
e.imageIds = e.imageIds || [];
for (const f of files) {
const id = 'img_' + memoId();
let meta = { name: f.name, type: f.type, size: f.size, lastModified: f.lastModified };
try {
if (typeof exifr !== 'undefined') {
const parsed = await exifr.parse(f, { translateValues: true, reviveValues: true, tiff: true, ifd0: true, exif: true, gps: true, xmp: true, iptc: true });
if (parsed) meta.exif = parsed;
}
} catch(_e){}
await memoDbSet('images', id, { blob: f, name: f.name, type: f.type, meta });
e.imageIds.push(id);
}
e.updatedAt = memoNowIso();
memoSaveLs();
memoSetStatus('画像を追加しました');
memoRenderPreview(e);
if (memoAutoSync) await memoSyncNow();
ev.target.value = '';
}
function memoSaveNew() {
const f = memoGetForm();
if (!f.prompt && !f.title) { memoSetStatus('タイトルかプロンプトを入力してください'); return; }
const id = memoId();
const now = memoNowIso();
memoEntries.push({ id, title: f.title, model: f.model, prompt: f.prompt, tags: f.tags, createdAt: now, updatedAt: now, imageIds: [] });
memoSaveLs();
memoSelectedId = id;
memoRenderList();
memoSelect(id);
memoSetStatus('新規保存しました');
if (memoAutoSync) memoSyncNow();
}
function memoUpdateSelected() {
const e = memoEntries.find(x=>x.id===memoSelectedId);
if (!e) { memoSetStatus('一覧から選択してください'); return; }
const f = memoGetForm();
e.title = f.title; e.model = f.model; e.prompt = f.prompt; e.tags = f.tags; e.updatedAt = memoNowIso();
memoSaveLs();
memoRenderList();
memoRenderPreview(e);
memoSetStatus('更新しました');
if (memoAutoSync) memoSyncNow();
}
async function memoDeleteSelected() {
const e = memoEntries.find(x=>x.id===memoSelectedId);
if (!e) return;
if (!confirm('この備忘録を削除しますか?')) return;
// 画像もDBから削除
for (const imgId of (e.imageIds||[])) { try { await memoDbDel('images', imgId); } catch(_e){} }
memoEntries = memoEntries.filter(x=>x.id!==memoSelectedId);
memoSelectedId = null;
memoSaveLs();
memoRenderList();
const prev = document.getElementById('memoPreview'); if (prev) { prev.className='output-area empty'; prev.textContent='左の一覧から選択してください'; }
const imgWrap = document.getElementById('memoImages'); if (imgWrap) imgWrap.innerHTML='';
const meta = document.getElementById('memoMeta'); if (meta) { meta.className='output-area empty'; meta.textContent='画像メタデータはここに表示'; }
memoSetStatus('削除しました');
if (memoAutoSync) memoSyncNow();
}
function memoUpdateSyncFolderLabel(name) {
const el = document.getElementById('memoSyncFolderName');
if (!el) return;
if (name) { el.textContent = '📂 ' + name; el.title = name; }
else { el.textContent = ''; el.title = ''; }
}
async function memoChooseSyncFolder() {
if (!('showDirectoryPicker' in window)) {
alert('同期フォルダは Chrome/Edge など File System Access API 対応ブラウザで利用できます。');
return;
}
try {
const handle = await window.showDirectoryPicker({ mode: 'readwrite' });
await memoDbSet('settings', 'syncDirHandle', handle);
memoUpdateSyncFolderLabel(handle.name);
memoSetStatus('同期フォルダを設定しました: ' + handle.name);
if (memoAutoSync) await memoSyncNow();
} catch(e) {
if (e.name !== 'AbortError') alert('フォルダ選択に失敗: ' + (e.message||e));
}
}
async function memoSyncNow() {
if (!('showDirectoryPicker' in window)) { memoSetStatus('このブラウザはフォルダ同期に非対応'); return; }
const handle = await memoDbGet('settings', 'syncDirHandle');
if (!handle) { memoSetStatus('同期フォルダが未設定'); return; }
try {
// ページリロード後のパーミッション再確認
let perm = await handle.queryPermission({ mode: 'readwrite' });
if (perm !== 'granted') {
perm = await handle.requestPermission({ mode: 'readwrite' });
if (perm !== 'granted') { memoSetStatus('フォルダへのアクセス権がありません'); return; }
}
memoUpdateSyncFolderLabel(handle.name);
// subfolder
const root = await handle.getDirectoryHandle('prompt_memo', { create: true });
const imgDir = await root.getDirectoryHandle('images', { create: true });
// write index
const idxHandle = await root.getFileHandle('index.json', { create: true });
const w = await idxHandle.createWritable();
await w.write(JSON.stringify({ version: 1, exportedAt: memoNowIso(), entries: memoEntries }, null, 2));
await w.close();
// write images
const ids = new Set();
memoEntries.forEach(e => (e.imageIds||[]).forEach(id => ids.add(id)));
for (const id of ids) {
const rec = await memoDbGet('images', id);
if (!rec || !rec.blob) continue;
const ext = (rec.name && rec.name.includes('.')) ? rec.name.split('.').pop() : (rec.type||'').split('/').pop() || 'bin';
const fileHandle = await imgDir.getFileHandle(id + '.' + ext, { create: true });
const wf = await fileHandle.createWritable();
await wf.write(rec.blob);
await wf.close();
// meta
const metaHandle = await imgDir.getFileHandle(id + '.meta.json', { create: true });
const wm = await metaHandle.createWritable();
await wm.write(JSON.stringify(rec.meta || {}, null, 2));
await wm.close();
}
memoSetStatus('フォルダ同期しました');
} catch(e) {
memoSetStatus('同期失敗: ' + (e.message || e));
}
}
function memoExportJson() {
const json = JSON.stringify({ version: 1, exportedAt: memoNowIso(), entries: memoEntries }, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'prompt_memo_' + new Date().toISOString().slice(0,10) + '.json';
a.click();
URL.revokeObjectURL(url);
memoSetStatus('JSONをダウンロードしました');
}
async function memoImportJson(ev) {
const f = ev.target?.files?.[0];
if (!f) return;
try {
const txt = await f.text();
const data = JSON.parse(txt);
if (!data || !Array.isArray(data.entries)) throw new Error('形式が不正です');
// merge by id (上書き)
const map = new Map(memoEntries.map(e=>[e.id,e]));
for (const e of data.entries) {
if (!e || !e.id) continue;
map.set(e.id, { ...map.get(e.id), ...e });
}
memoEntries = Array.from(map.values());
memoSaveLs();
memoRenderList();
memoSetStatus('JSONをインポートしました');
if (memoAutoSync) memoSyncNow();
} catch(e) {
alert('インポート失敗: ' + (e.message || e));
} finally {
ev.target.value = '';
}
}
/* ===== WILDCARD ===== */
const WILDCARD_NAMES = {
general: 'pf_general', angle: 'pf_angle', style: 'pf_style', chara: 'pf_chara_pose', chara_attr: 'pf_chara_attr',
character: 'pf_character',
appear_haircolor: 'pf_haircolor', appear_hairstyle: 'pf_hairstyle',
appear_eye: 'pf_appear_eye',
appear_face: 'pf_appear_face',
appear_skin: 'pf_appear_skin', appear_bodyshape: 'pf_appear_bodyshape', appear_accessory: 'pf_appear_accessory',
costume_casual: 'pf_costume_casual', costume_uniform: 'pf_costume_uniform', costume_fantasy: 'pf_costume_fantasy',
appear_height: 'pf_appear_height', appear_special: 'pf_appear_special',
nsfw_rating: 'pf_nsfw_rating', nsfw_body_f: 'pf_nsfw_body_f',
nsfw_body_m: 'pf_nsfw_body_m', nsfw_position: 'pf_nsfw_position',
nsfw_vaginal: 'pf_nsfw_vaginal', nsfw_oral: 'pf_nsfw_oral',
nsfw_anal: 'pf_nsfw_anal', nsfw_handjob: 'pf_nsfw_handjob',
nsfw_paizuri: 'pf_nsfw_paizuri', nsfw_cum: 'pf_nsfw_cum',
nsfw_squirt: 'pf_nsfw_squirt', nsfw_reaction: 'pf_nsfw_reaction',
nsfw_costume: 'pf_nsfw_costume', nsfw_bondage: 'pf_nsfw_bondage',
nsfw_yuri: 'pf_nsfw_yuri', nsfw_futa: 'pf_nsfw_futa',
nsfw_monster: 'pf_nsfw_monster', nsfw_scenario: 'pf_nsfw_scenario',
nsfw_fetish: 'pf_nsfw_fetish', nsfw_pose: 'pf_nsfw_pose',
nsfw_misc: 'pf_nsfw_misc',
hair: 'pf_hair', fetish: 'pf_fetish', lewd: 'pf_lewd', location: 'pf_location',
maniac_clothes: 'pf_maniac_clothes', artist: 'pf_artist', pose: 'pf_pose',
};
function getWildcardTxtEntries() {
const entries = [];
const seen = new Set();
for (const key of Object.keys(WILDCARD_NAMES)) {
const fname = WILDCARD_NAMES[key];
if (seen.has(fname)) continue;
seen.add(fname);
const tags = Array.isArray(PRESETS[key]) ? PRESETS[key] : [];
entries.push({ fname, content: tags.join('\n') });
}
if (!seen.has('pf_costume_all')) {
const all = [].concat(
PRESETS.costume_casual || [],
PRESETS.costume_uniform || [],
PRESETS.costume_fantasy || []
);
entries.push({ fname: 'pf_costume_all', content: all.join('\n') });
}
return entries;
}
async function exportWildcardsAsZip() {
if (typeof JSZip === 'undefined') {
alert('ZIP作成にJSZipが必要です。CDNがブロックされていないか確認してください。');
return;
}
const folder = (document.getElementById('wcExportFolder')?.value || 'wildcards').trim().replace(/\\/g, '/').replace(/\/+$/, '') || 'wildcards';
const zip = new JSZip();
const dir = folder ? zip.folder(folder) : zip;
for (const { fname, content } of getWildcardTxtEntries()) {
dir.file(fname + '.txt', content, { unixPermissions: '644' });
}
const blob = await zip.generateAsync({ type: 'blob' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'prompt_forge_wildcards_' + new Date().toISOString().slice(0,10) + '.zip';
a.click();
URL.revokeObjectURL(url);
if (document.getElementById('wcToast')) {
document.getElementById('wcToast').textContent = 'Wildcard TXT を ZIP でダウンロードしました';
document.getElementById('wcToast').classList.add('show');
clearTimeout(window._wcToastTimer);
window._wcToastTimer = setTimeout(function(){ document.getElementById('wcToast').classList.remove('show'); }, 2500);
}
}
async function exportWildcardsToFolder() {
if (!('showDirectoryPicker' in window)) {
alert('「フォルダに保存」は Chrome や Edge など、File System Access API に対応したブラウザで利用できます。');
return;
}
const subFolder = (document.getElementById('wcExportFolder')?.value || 'wildcards').trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, '') || 'wildcards';
try {
const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
const targetDir = subFolder ? await dirHandle.getDirectoryHandle(subFolder, { create: true }) : dirHandle;
const entries = getWildcardTxtEntries();
for (const { fname, content } of entries) {
const f = await targetDir.getFileHandle(fname + '.txt', { create: true });
const w = await f.createWritable();
await w.write(content);
await w.close();
}
if (document.getElementById('wcToast')) {
document.getElementById('wcToast').textContent = entries.length + ' 個の TXT をフォルダに保存しました';
document.getElementById('wcToast').classList.add('show');
clearTimeout(window._wcToastTimer);
window._wcToastTimer = setTimeout(function(){ document.getElementById('wcToast').classList.remove('show'); }, 2500);
}
} catch (e) {
if (e.name !== 'AbortError') alert('保存に失敗しました: ' + (e.message || e));
}
}
function insertWildcard(key, fname) {
const wc = '__' + (fname || WILDCARD_NAMES[key] || key) + '__';
const existIdx = selectedTags.findIndex(t => t.tag === wc);
if (existIdx >= 0) {
selectedTags.splice(existIdx, 1);
} else {
selectedTags.push({ tag: wc, preset: key });
showWcToast(wc);
}
renderSelZone(); renderTags(); updateTagCount(); updateFinal();
syncWcActiveStates();
}
function syncWcActiveStates() {
const activeWcTags = new Set(
selectedTags.filter(t => t.tag.startsWith('__') && t.tag.endsWith('__')).map(t => t.tag)
);
document.querySelectorAll('.wc-btn[data-wc]').forEach(btn => {
btn.classList.toggle('wc-active', activeWcTags.has(btn.dataset.wc));
});
document.querySelectorAll('.wc-shelf-btn[data-wc]').forEach(btn => {
btn.classList.toggle('wc-active', activeWcTags.has(btn.dataset.wc));
});
}
function wcShelfAllOn() {
document.querySelectorAll('.wc-shelf-btn[data-wc]').forEach(btn => {
const wc = btn.dataset.wc;
const fname = wc.replace(/^__|__$/g, '');
if (!selectedTags.find(t => t.tag === wc)) {
selectedTags.push({ tag: wc, preset: fname });
}
});
renderSelZone(); updateTagCount(); updateFinal(); syncWcActiveStates();
showWcToast('Wildcard 全項目 ON');
}
function wcShelfAllOff() {
const shelfWcs = new Set(
Array.from(document.querySelectorAll('.wc-shelf-btn[data-wc]')).map(b => b.dataset.wc)
);
selectedTags = selectedTags.filter(t => !shelfWcs.has(t.tag));
renderSelZone(); updateTagCount(); updateFinal(); syncWcActiveStates();
showWcToast('Wildcard 全項目 OFF');
}
function showWcToast(wc) {
const t = document.getElementById('wcToast');
if (!t) return;
t.textContent = wc + ' を追加しました';
t.classList.add('show');
clearTimeout(window._wcToastTimer);
window._wcToastTimer = setTimeout(() => t.classList.remove('show'), 1800);
}
/* ===== CHARA SEARCH ===== */
let charaSearchQuery = '';
function onCharaSearch() {
charaSearchQuery = (document.getElementById('charaSearchInput')?.value || '').toLowerCase().trim();
renderCharaSearchCount();
if (currentPreset === '_chara' || currentPreset === 'chara') renderTags();
}
function clearCharaSearch() {
const inp = document.getElementById('charaSearchInput');
if (inp) inp.value = '';
charaSearchQuery = '';
renderCharaSearchCount();
if (currentPreset === '_chara' || currentPreset === 'chara') renderTags();
}
function renderCharaSearchCount() {
const el = document.getElementById('charaSearchCount');
if (!el) return;
if (!charaSearchQuery) { el.textContent = ''; return; }
const tags = getDisplayTags();
el.textContent = tags.length + '件';
}
/* ===== INIT ===== */
function init() {
initCharaDropdown();
switchPreset('animagine');
renderSelZone(); renderNegSelZone(); renderSaves(); cpRender();
memoLoad();
if (apiKey) {
const inp = document.getElementById('apikeyInput');
if (inp) { inp.value = apiKey; updateKeyStatus(); }
}
}
/* =================================================================
===== FOOOCUS API 画像生成機能(追加ブロック) =====
================================================================= */
// ──────────────────────────────────────────────
// 状態変数
// ──────────────────────────────────────────────
/** Fooocus APIのエンドポイントURLローカルストレージに保存 */
let fooocusEndpoint = localStorage.getItem('pf_fooocus_endpoint') || 'http://127.0.0.1:8080';
/** 生成済み画像の配列 [{url, base64, pos, neg, params, timestamp}] */
let genGalleryItems = [];
/** 生成中タイマーID経過時間カウント用 */
let genElapsedTimer = null;
/** ユーザーが手動でプロンプトを編集したかのフラグ */
let genPromptManualEdit = false;
// ──────────────────────────────────────────────
// 初期化:エンドポイント入力欄の復元
// ──────────────────────────────────────────────
/**
* ページ読み込み時にlocalStorageからFooocus設定を復元する。
* init() 関数の末尾から呼ばれる想定だが、直接DOMContentLoadedでも動作する。
*/
function initFooocusUI() {
const inp = document.getElementById('fooocusEndpointInput');
if (inp) inp.value = fooocusEndpoint;
// 保存済みAPIパスを復元
const savedPath = localStorage.getItem('pf_fooocus_path');
if (savedPath) {
const sel = document.getElementById('fooocusApiPath');
if (sel) sel.value = savedPath;
}
}
// ──────────────────────────────────────────────
// エンドポイント入力のハンドラ変更をlocalStorageに即保存
// ──────────────────────────────────────────────
function onFooocusEndpointInput() {
const inp = document.getElementById('fooocusEndpointInput');
if (!inp) return;
fooocusEndpoint = inp.value.trim().replace(/\/$/, ''); // 末尾スラッシュを除去
localStorage.setItem('pf_fooocus_endpoint', fooocusEndpoint);
// APIパスも保存
const pathSel = document.getElementById('fooocusApiPath');
if (pathSel) localStorage.setItem('pf_fooocus_path', pathSel.value);
// ステータスをオフラインにリセット(未確認状態)
setFooocusStatus('offline', 'オフライン');
}
// ──────────────────────────────────────────────
// ステータスインジケーター制御
// ──────────────────────────────────────────────
/**
* @param {'online'|'offline'|'testing'} state
* @param {string} label 表示ラベル
*/
function setFooocusStatus(state, label) {
const dot = document.getElementById('fooocusDot');
const text = document.getElementById('fooocusStatusText');
const wrap = document.getElementById('fooocusStatus');
if (!dot || !text || !wrap) return;
dot.className = 'status-dot ' + state;
wrap.className = 'fooocus-status ' + state;
text.textContent = label;
}
// ──────────────────────────────────────────────
// 接続テストGETリクエストでルートまたはping相当を確認
// ──────────────────────────────────────────────
async function testFooocusConnection() {
const btn = document.getElementById('fooocusTestBtn');
if (btn) { btn.disabled = true; btn.textContent = '⏳ 確認中...'; }
setFooocusStatus('testing', 'テスト中...');
try {
// Fooocus REST APIの場合 /ping または / にGETを送る。
// タイムアウト5秒を AbortController で設定
const controller = new AbortController();
const tid = setTimeout(() => controller.abort(), 5000);
const endpoint = fooocusEndpoint || 'http://127.0.0.1:8080';
// fooocus-api は GET /ping で "pong" を返す(最優先)。
// /ping が404なら / も試すGradio UI等への対応
let connected = false;
let lastStatus = 0;
const candidates = [endpoint + '/ping', endpoint + '/'];
for (const testUrl of candidates) {
try {
const r = await fetch(testUrl, { method: 'GET', signal: controller.signal, mode: 'cors' });
lastStatus = r.status;
if (r.status < 500) { connected = true; break; } // 2xx/3xx/4xx ならサーバー稼働
} catch (_) { /* 次のURLを試す */ }
}
clearTimeout(tid);
if (connected) {
setFooocusStatus('online', 'オンライン ✓');
showSaveMsg('✓ Fooocus APIに接続できました', 'ok');
} else {
setFooocusStatus('offline', `HTTP ${lastStatus || 'ERR'}`);
}
} catch (e) {
if (e.name === 'AbortError') {
setFooocusStatus('offline', 'タイムアウト');
showSaveMsg('⚠ 接続タイムアウト5秒。Fooocusが起動しているか確認してください。', 'warn');
} else if (e.message && e.message.toLowerCase().includes('cors')) {
setFooocusStatus('offline', 'CORSエラー');
showGenError(
'CORSエラーが発生しました。\n' +
'Fooocusを以下の引数で起動してください:\n' +
' python launch.py --share または\n' +
' python launch.py --listen --cors-allow-origins *'
);
} else {
// NetworkErrorの場合はサーバー未起動の可能性大
setFooocusStatus('offline', '接続失敗');
showSaveMsg('⚠ Fooocusに接続できません。起動しているか、URLを確認してください。', 'warn');
}
} finally {
if (btn) { btn.disabled = false; btn.textContent = '⚡ 接続テスト'; }
}
}
// ──────────────────────────────────────────────
// プロンプト同期ヘルパー
// ──────────────────────────────────────────────
/** 最終プロンプトから生成入力欄に値を取得して反映 */
function syncGenFromFinal() {
genPromptManualEdit = false;
const pos = getFinalPos();
const neg = getFinalNeg();
const posEl = document.getElementById('genPosInput');
const negEl = document.getElementById('genNegInput');
if (posEl) posEl.value = pos;
if (negEl) negEl.value = neg;
}
/** ユーザーが手動で入力欄を編集したことを記録 */
function syncGenPromptManual() {
genPromptManualEdit = true;
}
// ──────────────────────────────────────────────
// シードランダム化
// ──────────────────────────────────────────────
function randomizeSeed() {
const el = document.getElementById('genSeed');
if (el) el.value = Math.floor(Math.random() * 2147483647);
}
// ──────────────────────────────────────────────
// エラー表示ヘルパー
// ──────────────────────────────────────────────
function showGenError(msg) {
const el = document.getElementById('genError');
const msg2 = document.getElementById('genErrorMsg');
if (!el || !msg2) return;
msg2.textContent = msg;
el.classList.add('active');
}
function hideGenError() {
const el = document.getElementById('genError');
if (el) el.classList.remove('active');
}
// ──────────────────────────────────────────────
// ローディングUI制御
// ──────────────────────────────────────────────
function showGenLoading() {
const el = document.getElementById('genLoading');
if (el) el.classList.add('active');
// 経過時間カウント開始
let sec = 0;
const elapsed = document.getElementById('genElapsed');
if (elapsed) elapsed.textContent = '経過時間: 0s';
genElapsedTimer = setInterval(() => {
sec++;
if (elapsed) elapsed.textContent = `経過時間: ${sec}s`;
}, 1000);
}
function hideGenLoading() {
const el = document.getElementById('genLoading');
if (el) el.classList.remove('active');
if (genElapsedTimer) { clearInterval(genElapsedTimer); genElapsedTimer = null; }
}
// ──────────────────────────────────────────────
// ボタン状態切り替え
// ──────────────────────────────────────────────
function setGenBtnLoading(flag) {
const btns = [
document.getElementById('genMainBtn'),
document.getElementById('genMinBtn')
];
btns.forEach(btn => {
if (!btn) return;
if (flag) {
btn.disabled = true;
btn._origText = btn.innerHTML;
btn.innerHTML = '<span class="spinner"></span> GENERATING...';
} else {
btn.disabled = false;
if (btn._origText) btn.innerHTML = btn._origText;
}
});
}
// ──────────────────────────────────────────────
// メイン:画像生成リクエスト
// ──────────────────────────────────────────────
async function generateImage() {
// プロンプトを取得(手動編集されていなければ最終プロンプトから)
let posPrompt, negPrompt;
const posEl = document.getElementById('genPosInput');
const negEl = document.getElementById('genNegInput');
if (posEl && posEl.value.trim()) {
posPrompt = posEl.value.trim();
} else {
posPrompt = getFinalPos();
}
if (negEl) {
negPrompt = negEl.value.trim() || getFinalNeg();
} else {
negPrompt = getFinalNeg();
}
if (!posPrompt) {
showGenError('プロンプトが空です。タグを選択するか、翻訳してください。');
return;
}
// APIエンドポイント確認
const endpoint = (fooocusEndpoint || 'http://127.0.0.1:8080').replace(/\/$/, '');
const pathSel = document.getElementById('fooocusApiPath');
const apiPath = pathSel ? pathSel.value : '/v1/generation/text-to-image';
// パラメータを収集
// アスペクト比はUIの値から w, h を取得(セレクタの値は "1152×896" 形式)
const aspectRaw = document.getElementById('genAspect')?.value || '1024×1024';
// UIの表示用区切り文字×を数値に変換
const aspectNums = aspectRaw.replace(/[×x]/g, ' ').trim().split(/\s+/).map(Number);
const w = aspectNums[0] || 1024;
const h = aspectNums[1] || 1024;
// fooocus-api の aspect_ratios_selection は "1152*896" のようにアスタリスク区切り
const aspectForApi = `${w}*${h}`;
const imageNum = parseInt(document.getElementById('genImageNum')?.value || '1', 10);
const seedRaw = parseInt(document.getElementById('genSeed')?.value || '-1', 10);
const actualSeed = seedRaw === -1 ? Math.floor(Math.random() * 2147483647) : seedRaw;
const steps = parseInt(document.getElementById('genSteps')?.value || '30', 10);
const cfg = parseFloat(document.getElementById('genCfg')?.value || '7');
const refiner = document.getElementById('genRefiner')?.checked !== false;
const style = document.getElementById('genStyle')?.value || 'Fooocus V2';
// ──── ペイロードを構築 ────
// fooocus-api REST API (/v1/generation/text-to-image) の形式
// 参照: https://github.com/mrhan1993/Fooocus-API
let payload;
if (apiPath.startsWith('/v1/')) {
payload = {
prompt: posPrompt,
negative_prompt: negPrompt || '',
style_selections: [style],
performance_selection: 'Speed',
// aspect_ratios_selection は "幅*高さ" 形式(アスタリスク区切り)
aspect_ratios_selection: aspectForApi,
image_number: imageNum,
image_seed: actualSeed,
sharpness: 2.0,
guidance_scale: cfg,
refiner_switch: refiner ? 0.5 : 1.0,
// async_process: false で同期レスポンス(タイムアウトに注意)
async_process: false,
require_base64: true // レスポンスにbase64を含める
};
} else {
// Gradio API 形式 (/run/predict, /api/predict)
payload = {
fn_index: 0, // Gradioの場合は fn_index を使う(ユーザーが調整要)
data: [
posPrompt,
negPrompt || '',
style,
aspectForApi,
imageNum,
'Speed',
actualSeed,
steps,
cfg,
refiner,
]
};
}
// ──── リクエスト送信 ────
setGenBtnLoading(true);
showGenLoading();
hideGenError();
const fullUrl = endpoint + apiPath;
console.log('[generateImage] POST', fullUrl, payload);
try {
const controller = new AbortController();
// タイムアウト300秒画像生成は時間がかかることがあるため長め
const tid = setTimeout(() => controller.abort(), 300000);
const res = await fetch(fullUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
mode: 'cors',
});
clearTimeout(tid);
// HTTPエラーチェック
if (!res.ok) {
let errText = `HTTP ${res.status} ${res.statusText}`;
try { const errJson = await res.json(); errText += '\n' + JSON.stringify(errJson, null, 2); } catch(_) {}
throw new Error(errText);
}
const data = await res.json();
console.log('[generateImage] response:', data);
// ──── レスポンスから画像URLまたはBase64を取得 ────
const images = extractImages(data);
if (!images.length) {
throw new Error('レスポンスに画像データが含まれていませんでした。\nレスポンス: ' + JSON.stringify(data).slice(0, 300));
}
// ギャラリーに追加
images.forEach(imgSrc => {
addToGallery({
src: imgSrc,
pos: posPrompt,
neg: negPrompt,
params: { w, h, imageNum, seed: actualSeed, steps, cfg, style },
timestamp: new Date().toLocaleTimeString('ja-JP'),
});
});
setFooocusStatus('online', 'オンライン ✓');
showSaveMsg(`${images.length}枚の画像を生成しました`, 'ok');
// 生成ページに切り替わっていない場合はページ5を表示
const p5 = document.getElementById('page5');
if (p5 && !p5.classList.contains('active')) switchPage(5);
} catch (e) {
console.error('[generateImage] error:', e);
let userMsg = e.message || String(e);
// CORSエラーの判定TypeError: Failed to fetch 等)
if (e.name === 'TypeError' || userMsg.toLowerCase().includes('failed to fetch') || userMsg.toLowerCase().includes('cors')) {
userMsg =
'CORSエラーまたはネットワークエラーが発生しました。\n\n' +
'以下を確認してください:\n' +
'① Fooocusが起動しているか\n' +
'② 起動引数に --cors-allow-origins * または --share が含まれているか\n' +
' 例: python launch.py --listen --cors-allow-origins *\n' +
'③ URLとポートが正しいかデフォルト: http://127.0.0.1:8080';
setFooocusStatus('offline', 'CORSエラー');
} else if (e.name === 'AbortError') {
userMsg = '生成リクエストがタイムアウトしました300秒。\nFooocusのログを確認してください。';
setFooocusStatus('offline', 'タイムアウト');
} else {
setFooocusStatus('offline', 'エラー');
}
showGenError(userMsg);
} finally {
setGenBtnLoading(false);
hideGenLoading();
}
}
// ──────────────────────────────────────────────
// レスポンスから画像データを抽出するヘルパー
// Fooocus の REST / Gradio どちらにも対応
// ──────────────────────────────────────────────
function extractImages(data) {
const results = [];
const base = (fooocusEndpoint || 'http://127.0.0.1:8080');
/**
* 1つのアイテムから画像ソースを取り出す共通ヘルパー
* fooocus-api のレスポンス例require_base64:true:
* { "base64": "<base64文字列>", "seed": 12345, "finish_reason": "SUCCESS", "meta": {} }
* require_base64:false の場合:
* { "url": "http://...", "seed": ..., "finish_reason": "SUCCESS" }
*/
function extractOne(item) {
if (!item) return null;
if (typeof item === 'string') return normalizeImgSrc(item);
// finish_reason が SUCCESS 以外はスキップ(エラー画像を除外)
if (item.finish_reason && item.finish_reason !== 'SUCCESS') return null;
// base64 フィールドfooocus-api の主要なレスポンス形式)
if (item.base64) {
const b64 = item.base64;
// 既に "data:" URIのものはそのまま、純粋なbase64はプレフィックスを付ける
return b64.startsWith('data:') ? b64 : 'data:image/png;base64,' + b64;
}
// url フィールドrequire_base64:false 時)
if (item.url) {
// 相対パスなら絶対URLに変換
return item.url.startsWith('http') ? item.url : base + item.url;
}
// Gradio ファイル参照形式 { name: "tmp/xxx.png", is_file: true }
if (item.name) {
return item.is_file
? base + '/file=' + item.name
: ('data:image/png;base64,' + item.name); // nameがbase64の場合
}
// Gradio data フィールド形式 { data: "<base64>", is_file: false }
if (item.data) {
return item.is_file
? base + '/file=' + item.data
: (item.data.startsWith('data:') ? item.data : 'data:image/png;base64,' + item.data);
}
// 旧Fooocusのファイル名のみ形式
if (item.filename) return base + '/outputs/' + item.filename;
return null;
}
// ── 配列形式fooocus-api の標準レスポンス)──
if (Array.isArray(data)) {
data.forEach(item => {
const src = extractOne(item);
if (src) results.push(src);
});
}
// ── Gradio 形式: { data: [[...], ...] } ──
else if (data && Array.isArray(data.data)) {
data.data.flat(3).forEach(item => {
const src = extractOne(item);
if (src) results.push(src);
});
}
// ── 単一オブジェクト形式 ──
else if (data && typeof data === 'object') {
const src = extractOne(data);
if (src) results.push(src);
}
return results.filter(Boolean);
}
/**
* 画像ソース文字列を正規化するURLならそのまま、base64ならdata URIにする
* @param {string} src
*/
function normalizeImgSrc(src) {
if (!src) return '';
if (src.startsWith('data:')) return src; // 既にdata URI
if (src.startsWith('http://') || src.startsWith('https://') || src.startsWith('/')) return src;
// 純粋なbase64文字列と判定
return 'data:image/png;base64,' + src;
}
// ──────────────────────────────────────────────
// ギャラリーへのアイテム追加
// ──────────────────────────────────────────────
/**
* @param {{ src:string, pos:string, neg:string, params:object, timestamp:string }} item
*/
function addToGallery(item) {
genGalleryItems.unshift(item); // 新しいものを先頭に追加
renderGallery();
}
function clearGallery() {
if (!genGalleryItems.length) return;
if (!confirm('ギャラリーをすべてクリアしますか?')) return;
genGalleryItems = [];
renderGallery();
}
function renderGallery() {
const gallery = document.getElementById('genGallery');
const countEl = document.getElementById('genGalleryCount');
if (!gallery) return;
if (countEl) countEl.textContent = genGalleryItems.length ? `(${genGalleryItems.length}枚)` : '';
if (!genGalleryItems.length) {
gallery.innerHTML = '<div class="gen-gallery-empty">生成した画像がここに表示されます</div>';
return;
}
// 1列あたり2枚程度のグリッドで表示
gallery.innerHTML = genGalleryItems.map((item, idx) => {
const shortPos = (item.pos || '').slice(0, 60) + ((item.pos || '').length > 60 ? '…' : '');
return `
<div class="gen-gallery-item" style="width:calc(50% - 5px);">
${item.posted ? '<span class="posted-badge">𝕏 投稿済み</span>' : ''}
<img src="${escHtml(item.src)}"
alt="Generated image ${idx + 1}"
loading="lazy"
onclick="openImgModal('${escHtml(item.src)}')"
title="${escHtml(shortPos)}">
<div class="gen-img-actions">
<button class="gen-img-btn save-to-memo" onclick="event.stopPropagation();genSaveToMemo(${idx})">💾 備忘録</button>
<button class="gen-img-btn" onclick="event.stopPropagation();genDownloadImage(${idx})">⬇ DL</button>
<button class="gen-img-btn" onclick="event.stopPropagation();genCopyPrompt(${idx})">📋 POS</button>
<button class="gen-img-btn" onclick="event.stopPropagation();openColorModal(${idx})" style="border-color:rgba(0,229,255,.6);color:var(--accent3);">🎨 補正</button>
<button class="gen-img-btn" onclick="event.stopPropagation();openXModal(${idx},null)" style="border-color:rgba(29,161,242,.6);color:#1da1f2;">𝕏 投稿</button>
</div>
</div>
`;
}).join('');
}
// ──────────────────────────────────────────────
// 画像拡大モーダル
// ──────────────────────────────────────────────
function openImgModal(src) {
const modal = document.getElementById('genImgModal');
const img = document.getElementById('genImgModalSrc');
if (!modal || !img) return;
img.src = src;
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeImgModal() {
const modal = document.getElementById('genImgModal');
if (modal) modal.classList.remove('active');
document.body.style.overflow = '';
}
// ──────────────────────────────────────────────
// ギャラリー画像を備忘録ページ4に保存
// ──────────────────────────────────────────────
/**
* 生成した画像とプロンプトをページ4の備忘録に自動保存する
* @param {number} idx ギャラリーインデックス
*/
async function genSaveToMemo(idx) {
const item = genGalleryItems[idx];
if (!item) return;
// 備忘録のフォームにデータをセット
const titleEl = document.getElementById('memoTitle');
const promptEl = document.getElementById('memoPrompt');
if (titleEl) titleEl.value = `生成_${item.timestamp || new Date().toLocaleTimeString('ja-JP')}`;
if (promptEl) {
let promptText = '';
if (item.pos) promptText += '[POS] ' + item.pos;
if (item.neg) promptText += '\n[NEG] ' + item.neg;
if (item.params) {
const p = item.params;
promptText += `\n[PARAMS] ${p.w}x${p.h}, ${p.imageNum}枚, seed:${p.seed}, steps:${p.steps}, cfg:${p.cfg}, style:${p.style}`;
}
promptEl.value = promptText.trim();
}
// 新規保存
if (typeof memoSaveNew === 'function') memoSaveNew();
// 画像をBase64でIndexedDBに保存memoAddImagesの代わりに直接呼ぶ
if (item.src) {
await genSaveImageToMemo(item.src);
}
// ページ4に切り替え
switchPage(4);
showSaveMsg('💾 画像とプロンプトを備忘録に保存しました', 'ok');
}
/**
* 画像ソースURL or base64をBlobに変換してmemoDbSetに渡す
*/
async function genSaveImageToMemo(src) {
try {
let blob;
if (src.startsWith('data:')) {
// base64 → Blob に変換
const res2 = await fetch(src);
blob = await res2.blob();
} else {
// URLからfetchして取得
const res2 = await fetch(src);
blob = await res2.blob();
}
const file = new File([blob], `generated_${Date.now()}.png`, { type: 'image/png' });
// memoAddImages と同等の処理
const id = 'img_' + (typeof memoId === 'function' ? memoId() : Date.now());
await memoDbSet('images', id, { blob: file, name: file.name, type: file.type, meta: { name: file.name, type: file.type } });
// 選択中のエントリに imageId を追加
if (typeof memoSelectedId !== 'undefined' && memoSelectedId) {
const entry = (typeof memoEntries !== 'undefined' ? memoEntries : []).find(x => x.id === memoSelectedId);
if (entry) {
entry.imageIds = entry.imageIds || [];
entry.imageIds.push(id);
entry.updatedAt = new Date().toISOString();
if (typeof memoSaveLs === 'function') memoSaveLs();
if (typeof memoRenderPreview === 'function') memoRenderPreview(entry);
}
}
} catch (e) {
console.warn('[genSaveImageToMemo] 画像の保存に失敗:', e);
}
}
// ──────────────────────────────────────────────
// 画像ダウンロード
// ──────────────────────────────────────────────
function genDownloadImage(idx) {
const item = genGalleryItems[idx];
if (!item || !item.src) return;
const a = document.createElement('a');
a.href = item.src;
a.download = `fooocus_${Date.now()}.png`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// ──────────────────────────────────────────────
// プロンプトをクリップボードにコピー
// ──────────────────────────────────────────────
function genCopyPrompt(idx) {
const item = genGalleryItems[idx];
if (!item) return;
let text = '';
if (item.pos) text += '[POS] ' + item.pos;
if (item.neg) text += '\n[NEG] ' + item.neg;
navigator.clipboard.writeText(text).then(() => {
showSaveMsg('📋 プロンプトをコピーしました', 'ok');
}).catch(() => {
showSaveMsg('⚠ コピーに失敗しました', 'warn');
});
}
window.genCopyPrompt = genCopyPrompt;
// ──────────────────────────────────────────────
// openImgModal をグローバルに公開HTML内から直接呼ぶ
// ──────────────────────────────────────────────
window.openImgModal = openImgModal;
// ──────────────────────────────────────────────
// DOMContentLoaded 後にFooocusUIを初期化
// ──────────────────────────────────────────────
(function () {
function _initFooocus() {
initFooocusUI();
// 最初のsyncGenFromFinalはページ切り替え時に行うためここではスキップ
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', _initFooocus);
} else {
_initFooocus();
}
})();
/* ===== GLOBAL EXPORTS (inline onclick access) ===== */
/* ===== GLOBAL EXPORTS (inline onclick access) ===== */
window.doTranslate = doTranslate;
window.insertWildcard = insertWildcard;
window.onCharaSearch = onCharaSearch;
window.clearCharaSearch = clearCharaSearch;
window.switchPreset = switchPreset;
window.toggleTag = toggleTag;
window.toggleSelectAll = toggleSelectAll;
window.toggleSelectAllPreset = toggleSelectAllPreset;
window.randomOne = randomOne;
window.randomAllNsfw = randomAllNsfw;
window.removeSelTag = removeSelTag;
window.removeNegSelTag = removeNegSelTag;
window.toggleLock = toggleLock;
window.dragStart = dragStart;
window.dragOver = dragOver;
window.dragDrop = dragDrop;
window.dragEnd = dragEnd;
window.appendToFinal = appendToFinal;
window.copyBtn = copyBtn;
window.clearTags = clearTags;
window.resetAll = resetAll;
window.toggleColl = toggleColl;
window.onKeyInput = onKeyInput;
window.onSearch = onSearch;
window.clearSearch = clearSearch;
window.onCharaSeriesChange= onCharaSeriesChange;
window.doTranslate = doTranslate;
window.insertWildcard = insertWildcard;
window.syncWcActiveStates = syncWcActiveStates;
window.wcShelfAllOn = wcShelfAllOn;
window.wcShelfAllOff = wcShelfAllOff;
window.exportWildcardsAsZip = exportWildcardsAsZip;
window.exportWildcardsToFolder = exportWildcardsToFolder;
window.switchPage = switchPage;
window.switchSubMode = switchSubMode;
window.memoAddImages = memoAddImages;
window.memoSaveNew = memoSaveNew;
window.memoUpdateSelected = memoUpdateSelected;
window.memoDeleteSelected = memoDeleteSelected;
window.memoSelect = memoSelect;
window.memoCopyPrompt = memoCopyPrompt;
window.memoCopySummary = memoCopySummary;
window.memoRenderList = memoRenderList;
window.memoChooseSyncFolder = memoChooseSyncFolder;
window.memoSyncNow = memoSyncNow;
window.memoExportJson = memoExportJson;
window.memoImportJson = memoImportJson;
window.memoSetAutoSync = memoSetAutoSync;
window.memoTagKeydown = memoTagKeydown;
window.memoTagAddFromPreset = memoTagAddFromPreset;
window.memoTagRemove = memoTagRemove;
window.memoSetTagFilter = memoSetTagFilter;
window.onCharaSearch = onCharaSearch;
window.clearCharaSearch = clearCharaSearch;
window.onImageSelect = onImageSelect;
window.clearImageInput = clearImageInput;
window.openAddImg = openAddImg;
window.onAddImgSelect = onAddImgSelect;
window.savePrompt = savePrompt;
window.testFooocusConnection = testFooocusConnection;
window.generateImage = generateImage;
// ══════════════════════════════════════════════════════════════
// 🎨 色調補正
// ══════════════════════════════════════════════════════════════
let colorModalGalleryIdx = null;
function openColorModal(idx) {
const item = genGalleryItems[idx];
if (!item || !item.src) return;
colorModalGalleryIdx = idx;
const img = document.getElementById('colorPreviewImg');
if (img) { img.src = item.src; img.style.filter = ''; }
// スライダーとプリセットをリセット
_resetColorSliders();
updateColorFilter();
document.querySelectorAll('.color-preset-btn').forEach(b => b.classList.remove('active'));
const first = document.querySelector('.color-preset-btn');
if (first) first.classList.add('active');
document.getElementById('colorModalOverlay').classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeColorModal() {
document.getElementById('colorModalOverlay').classList.remove('active');
document.body.style.overflow = '';
}
function _resetColorSliders() {
document.getElementById('brightnessSlider').value = 100;
document.getElementById('contrastSlider').value = 100;
document.getElementById('saturationSlider').value = 100;
document.getElementById('hueSlider').value = 0;
document.getElementById('sepiaSlider').value = 0;
}
function resetColorFilter() {
_resetColorSliders();
updateColorFilter();
document.querySelectorAll('.color-preset-btn').forEach(b => b.classList.remove('active'));
const first = document.querySelector('.color-preset-btn');
if (first) first.classList.add('active');
}
function updateColorFilter() {
const b = document.getElementById('brightnessSlider').value;
const c = document.getElementById('contrastSlider').value;
const s = document.getElementById('saturationSlider').value;
const h = document.getElementById('hueSlider').value;
const sp = document.getElementById('sepiaSlider').value;
document.getElementById('brightnessVal').textContent = b + '%';
document.getElementById('contrastVal').textContent = c + '%';
document.getElementById('saturationVal').textContent = s + '%';
document.getElementById('hueVal').textContent = h + '°';
document.getElementById('sepiaVal').textContent = sp + '%';
const filterStr = `brightness(${b}%) contrast(${c}%) saturate(${s}%) hue-rotate(${h}deg) sepia(${sp}%)`;
const img = document.getElementById('colorPreviewImg');
if (img) img.style.filter = filterStr;
}
function applyColorPreset(name, btn) {
const presets = {
normal: { b:100, c:100, s:100, h:0, sp:0 },
vivid: { b:105, c:115, s:160, h:0, sp:0 },
cinema: { b:95, c:120, s:80, h:-5, sp:15 },
anime: { b:105, c:110, s:140, h:10, sp:0 },
warm: { b:108, c:105, s:115, h:15, sp:10 },
cool: { b:102, c:108, s:100, h:-20, sp:0 },
hdr: { b:100, c:140, s:130, h:0, sp:0 },
};
const p = presets[name];
if (!p) return;
document.getElementById('brightnessSlider').value = p.b;
document.getElementById('contrastSlider').value = p.c;
document.getElementById('saturationSlider').value = p.s;
document.getElementById('hueSlider').value = p.h;
document.getElementById('sepiaSlider').value = p.sp;
updateColorFilter();
document.querySelectorAll('.color-preset-btn').forEach(b => b.classList.remove('active'));
if (btn) btn.classList.add('active');
}
async function _getCorrectedDataUrl() {
const img = document.getElementById('colorPreviewImg');
if (!img || !img.src) return null;
const b = parseInt(document.getElementById('brightnessSlider').value);
const c = parseInt(document.getElementById('contrastSlider').value);
const s = parseInt(document.getElementById('saturationSlider').value);
const h = parseInt(document.getElementById('hueSlider').value);
const sp = parseInt(document.getElementById('sepiaSlider').value);
// フィルターがデフォルトならオリジナルをそのまま返す
if (b === 100 && c === 100 && s === 100 && h === 0 && sp === 0) {
return img.src;
}
return new Promise(resolve => {
const canvas = document.createElement('canvas');
const tmp = new Image();
tmp.crossOrigin = 'anonymous';
tmp.onload = function() {
canvas.width = tmp.naturalWidth;
canvas.height = tmp.naturalHeight;
const ctx = canvas.getContext('2d');
ctx.filter = `brightness(${b}%) contrast(${c}%) saturate(${s}%) hue-rotate(${h}deg) sepia(${sp}%)`;
ctx.drawImage(tmp, 0, 0);
try {
resolve(canvas.toDataURL('image/png'));
} catch (e) {
// tainted canvas (CORS) → フィルターなしで返す
resolve(img.src);
}
};
tmp.onerror = function() { resolve(img.src); };
tmp.src = img.src;
});
}
async function downloadCorrectedImage() {
const dataUrl = await _getCorrectedDataUrl();
if (!dataUrl) return;
const a = document.createElement('a');
a.href = dataUrl;
a.download = 'fooocus_corrected_' + Date.now() + '.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
async function sendToXFromColor() {
const dataUrl = await _getCorrectedDataUrl();
const idx = colorModalGalleryIdx;
closeColorModal();
openXModal(idx, dataUrl);
}
window.openColorModal = openColorModal;
window.closeColorModal = closeColorModal;
window.updateColorFilter = updateColorFilter;
window.applyColorPreset = applyColorPreset;
window.resetColorFilter = resetColorFilter;
window.downloadCorrectedImage = downloadCorrectedImage;
window.sendToXFromColor = sendToXFromColor;
// ══════════════════════════════════════════════════════════════
// 𝕏 X (Twitter) 投稿
// ══════════════════════════════════════════════════════════════
let xModalGalleryIdx = null;
let xModalImageSrc = null;
let xSelectedHashtags = new Set();
function openXModal(idx, overrideSrc) {
const item = genGalleryItems[idx];
if (!item) return;
xModalGalleryIdx = idx;
xModalImageSrc = overrideSrc || item.src;
xSelectedHashtags = new Set();
// サムネイル
const thumb = document.getElementById('xPreviewThumb');
if (thumb) thumb.src = item.src;
// キャプション自動生成プロンプト先頭80文字
const caption = document.getElementById('xCaption');
if (caption) {
caption.value = item.pos ? item.pos.slice(0, 80) : '';
updateXCharCount();
}
// ハッシュタグ・メッセージをリセット
document.querySelectorAll('.x-hashtag-btn').forEach(b => b.classList.remove('selected'));
const msg = document.getElementById('xPostMsg');
if (msg) msg.style.display = 'none';
document.getElementById('xModalOverlay').classList.add('active');
document.body.style.overflow = 'hidden';
}
function closeXModal() {
document.getElementById('xModalOverlay').classList.remove('active');
document.body.style.overflow = '';
}
function updateXCharCount() {
const caption = document.getElementById('xCaption');
const countEl = document.getElementById('xCharCount');
if (!caption || !countEl) return;
const tags = xSelectedHashtags.size ? ' ' + Array.from(xSelectedHashtags).join(' ') : '';
const fullLen = caption.value.length + tags.length;
countEl.textContent = fullLen + ' / 280';
countEl.classList.toggle('over', fullLen > 280);
}
function toggleHashtag(btn, tag) {
if (xSelectedHashtags.has(tag)) {
xSelectedHashtags.delete(tag);
btn.classList.remove('selected');
} else {
xSelectedHashtags.add(tag);
btn.classList.add('selected');
}
updateXCharCount();
}
async function executeXPost() {
const item = genGalleryItems[xModalGalleryIdx];
if (!item) return;
const captionEl = document.getElementById('xCaption');
const captionText = captionEl ? captionEl.value.trim() : '';
const tags = xSelectedHashtags.size ? '\n' + Array.from(xSelectedHashtags).join(' ') : '';
const fullText = (captionText + tags).slice(0, 280);
// ① 画像をダウンロード
const src = xModalImageSrc || item.src;
const a = document.createElement('a');
a.href = src;
a.download = 'fooocus_x_' + Date.now() + '.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// ② キャプションをクリップボードにコピー
try {
await navigator.clipboard.writeText(fullText);
} catch (e) {
const ta = document.createElement('textarea');
ta.value = fullText;
ta.style.cssText = 'position:fixed;opacity:0;';
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
// ③ X Web Intent でタブを開く
const xUrl = 'https://x.com/intent/tweet?text=' + encodeURIComponent(fullText);
window.open(xUrl, '_blank', 'noopener,noreferrer');
// ④ 投稿済みバッジを付与してギャラリーを再描画
if (genGalleryItems[xModalGalleryIdx]) {
genGalleryItems[xModalGalleryIdx].posted = true;
renderGallery();
}
// フィードバック表示
const msg = document.getElementById('xPostMsg');
if (msg) {
msg.style.display = 'block';
msg.style.background = 'rgba(29,161,242,.1)';
msg.style.border = '1px solid rgba(29,161,242,.3)';
msg.style.color = '#1da1f2';
msg.style.borderRadius = '6px';
msg.style.padding = '8px';
msg.textContent = '✓ 画像をDL・キャプションをコピーしました。X.comが開きました';
}
}
window.openXModal = openXModal;
window.closeXModal = closeXModal;
window.updateXCharCount = updateXCharCount;
window.toggleHashtag = toggleHashtag;
window.executeXPost = executeXPost;
// ── Escキー / Ctrl+Enter ──
document.addEventListener('keydown', function(e) {
// Esc でモーダルを閉じる
if (e.key === 'Escape') {
const xOverlay = document.getElementById('xModalOverlay');
const colorOverlay = document.getElementById('colorModalOverlay');
if (xOverlay && xOverlay.classList.contains('active')) { closeXModal(); return; }
if (colorOverlay && colorOverlay.classList.contains('active')) { closeColorModal(); return; }
}
// Ctrl+Enter で X投稿モーダルを実行
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
const xOverlay = document.getElementById('xModalOverlay');
if (xOverlay && xOverlay.classList.contains('active')) {
e.preventDefault();
executeXPost();
}
}
});
// ── スライダーをダブルクリックで個別リセット ──
(function() {
const defaults = {
brightnessSlider: 100, contrastSlider: 100,
saturationSlider: 100, hueSlider: 0, sepiaSlider: 0
};
Object.entries(defaults).forEach(function([id, def]) {
document.addEventListener('DOMContentLoaded', function() {
const el = document.getElementById(id);
if (el) el.addEventListener('dblclick', function() {
el.value = def;
updateColorFilter();
// プリセットをリセットカスタムになったのでactiveを外す
document.querySelectorAll('.color-preset-btn').forEach(b => b.classList.remove('active'));
});
});
});
})();
// ── 色調補正後の画像をギャラリーに追加保存 ──
async function saveColorToGallery() {
const item = genGalleryItems[colorModalGalleryIdx];
if (!item) return;
const dataUrl = await _getCorrectedDataUrl();
if (!dataUrl) return;
const b = document.getElementById('brightnessSlider').value;
const c = document.getElementById('contrastSlider').value;
const s = document.getElementById('saturationSlider').value;
const h = document.getElementById('hueSlider').value;
const sp = document.getElementById('sepiaSlider').value;
const filterLabel = `B${b} C${c} S${s} H${h} Sp${sp}`;
addToGallery({
src: dataUrl,
pos: item.pos,
neg: item.neg,
params: item.params,
timestamp: new Date().toLocaleTimeString('ja-JP') + ' [補正]',
corrected: true,
filterLabel,
});
// トースト表示
const toast = document.getElementById('wcToast');
if (toast) {
toast.textContent = '✓ 補正済み画像をギャラリーに追加しました';
toast.classList.add('show');
clearTimeout(window._wcToastTimer);
window._wcToastTimer = setTimeout(function() { toast.classList.remove('show'); }, 2500);
}
closeColorModal();
}
window.saveColorToGallery = saveColorToGallery;
// ── updateColorFilter にthrottle適用rAF利用 ──
(function() {
const _orig = window.updateColorFilter;
let _pending = false;
window.updateColorFilter = function() {
if (_pending) return;
_pending = true;
requestAnimationFrame(function() {
_orig();
_pending = false;
});
};
})();
window.syncGenFromFinal = syncGenFromFinal;
window.syncGenPromptManual = syncGenPromptManual;
window.randomizeSeed = randomizeSeed;
window.clearGallery = clearGallery;
window.closeImgModal = closeImgModal;
window.genSaveToMemo = genSaveToMemo;
window.genDownloadImage = genDownloadImage;
window.onFooocusEndpointInput = onFooocusEndpointInput;
window.loadSave = loadSave;
window.deleteSave = deleteSave;
window.expandImg = expandImg;
window.restoreHist = restoreHist;
window.clearHistory = clearHistory;
window.cpApply = cpApply;
window.cpSaveCurrent = cpSaveCurrent;
window.cpDeleteUser = cpDeleteUser;
/* ===== INIT ===== */
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ══════════════════════════════════════════════
// キャラクタープリセット
// ══════════════════════════════════════════════
const CP_BUILTIN = [
{ id:'jk', name:'女子高生', icon:'🎒', desc:'セーラー服の清楚な女子高生',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'sailor uniform',preset:'costume_uniform'},
{tag:'black hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'brown eyes',preset:'appear_eye'},{tag:'innocent',preset:'chara_attr'},
{tag:'upper body',preset:'angle'}], negTags:[] },
{ id:'knight', name:'女騎士', icon:'⚔️', desc:'鎧を纏った凛々しい女騎士',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'knight armor',preset:'costume_fantasy'},{tag:'full plate armor',preset:'costume_fantasy'},
{tag:'silver hair',preset:'appear_haircolor'},{tag:'short hair',preset:'appear_hairstyle'},
{tag:'blue eyes',preset:'appear_eye'},{tag:'serious',preset:'chara_attr'},
{tag:'confident',preset:'chara_attr'},{tag:'full body',preset:'angle'}], negTags:[] },
{ id:'maid', name:'メイド', icon:'🫖', desc:'清楚で従順なメイドさん',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'maid uniform',preset:'costume_uniform'},
{tag:'black hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'brown eyes',preset:'appear_eye'},{tag:'gentle',preset:'chara_attr'},
{tag:'shy',preset:'chara_attr'},{tag:'upper body',preset:'angle'}], negTags:[] },
{ id:'maho', name:'魔法少女', icon:'✨', desc:'キラキラ魔法少女',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'magical girl outfit',preset:'costume_fantasy'},
{tag:'pink hair',preset:'appear_haircolor'},{tag:'twintails',preset:'appear_hairstyle'},
{tag:'sparkling eyes',preset:'appear_eye'},{tag:'cheerful',preset:'chara_attr'},
{tag:'energetic',preset:'chara_attr'}], negTags:[] },
{ id:'elf', name:'エルフ', icon:'🌿', desc:'神秘的な森のエルフ',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'elf outfit',preset:'costume_fantasy'},
{tag:'silver hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'green eyes',preset:'appear_eye'},{tag:'elf ears',preset:'appear_special'},
{tag:'calm',preset:'chara_attr'},{tag:'full body',preset:'angle'}], negTags:[] },
{ id:'kunoichi', name:'くのいち', icon:'🌸', desc:'影に生きる忍の女',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'kunoichi outfit',preset:'costume_uniform'},
{tag:'black hair',preset:'appear_haircolor'},{tag:'ponytail',preset:'appear_hairstyle'},
{tag:'purple eyes',preset:'appear_eye'},{tag:'serious',preset:'chara_attr'}], negTags:[] },
{ id:'witch', name:'魔女', icon:'🧙', desc:'神秘と闇の魔女',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'witch outfit',preset:'costume_fantasy'},
{tag:'purple hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'red eyes',preset:'appear_eye'},{tag:'mysterious',preset:'chara_attr'},
{tag:'hat',preset:'appear_accessory'}], negTags:[] },
{ id:'princess', name:'プリンセス', icon:'👑', desc:'気品あふれる王国の姫',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'princess dress',preset:'costume_fantasy'},
{tag:'golden hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'blue eyes',preset:'appear_eye'},{tag:'ojou-sama',preset:'chara_attr'},
{tag:'kind',preset:'chara_attr'},{tag:'tiara',preset:'appear_accessory'}], negTags:[] },
{ id:'vampire', name:'ヴァンパイア',icon:'🦇', desc:'永遠の夜を生きる吸血鬼',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'gothic lolita',preset:'costume_uniform'},
{tag:'silver hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'red eyes',preset:'appear_eye'},{tag:'vampire',preset:'appear_special'},
{tag:'mysterious',preset:'chara_attr'},{tag:'pale skin',preset:'appear_skin'}], negTags:[] },
{ id:'biker', name:'ヤンキー', icon:'🏍️', desc:'ガラ悪めなバイカーギャル',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'leather jacket',preset:'costume_casual'},
{tag:'white hair',preset:'appear_haircolor'},{tag:'short hair',preset:'appear_hairstyle'},
{tag:'red eyes',preset:'appear_eye'},{tag:'bold',preset:'chara_attr'},
{tag:'confident',preset:'chara_attr'}], negTags:[] },
{ id:'ojousama', name:'お嬢様', icon:'🌺', desc:'ゴージャスな貴族令嬢',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'lolita dress',preset:'costume_uniform'},
{tag:'blonde hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'violet eyes',preset:'appear_eye'},{tag:'ojou-sama',preset:'chara_attr'},
{tag:'gentle',preset:'chara_attr'},{tag:'tiara',preset:'appear_accessory'}], negTags:[] },
{ id:'nurse', name:'ナース', icon:'💉', desc:'優しいナースのお姉さん',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'nurse uniform',preset:'costume_uniform'},
{tag:'white hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'pink eyes',preset:'appear_eye'},{tag:'kind',preset:'chara_attr'},
{tag:'gentle',preset:'chara_attr'},{tag:'upper body',preset:'angle'}], negTags:[] },
{ id:'miko', name:'巫女', icon:'⛩️', desc:'神社に仕える清楚な巫女',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'miko outfit',preset:'costume_uniform'},
{tag:'black hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'brown eyes',preset:'appear_eye'},{tag:'innocent',preset:'chara_attr'},
{tag:'gentle',preset:'chara_attr'}], negTags:[] },
{ id:'kimono', name:'着物美人', icon:'🎋', desc:'和の心を持つ大和撫子',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'kimono',preset:'costume_uniform'},
{tag:'black hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'brown eyes',preset:'appear_eye'},{tag:'calm',preset:'chara_attr'},
{tag:'mature',preset:'chara_attr'}], negTags:[] },
{ id:'cyber', name:'サイバーパンク',icon:'⚡',desc:'ネオン輝くサイバーガール',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'cyberpunk outfit',preset:'costume_fantasy'},
{tag:'silver hair',preset:'appear_haircolor'},{tag:'short hair',preset:'appear_hairstyle'},
{tag:'glowing eyes',preset:'appear_eye'},{tag:'bold',preset:'chara_attr'}], negTags:[] },
{ id:'warrior', name:'戦士', icon:'🗡️', desc:'荒野を駆ける女戦士',
tags:[{tag:'1girl',preset:'general'},{tag:'looking at viewer',preset:'general'},
{tag:'barbarian outfit',preset:'costume_fantasy'},{tag:'warrior outfit',preset:'costume_fantasy'},
{tag:'red hair',preset:'appear_haircolor'},{tag:'long hair',preset:'appear_hairstyle'},
{tag:'gold eyes',preset:'appear_eye'},{tag:'brave',preset:'chara_attr'},
{tag:'muscular',preset:'appear_bodyshape'},{tag:'full body',preset:'angle'}], negTags:[] },
];
let cpUserPresets = (function(){
try{ return JSON.parse(localStorage.getItem('pf_char_presets')||'[]')||[]; }catch(e){ return []; }
})();
let cpActiveId = null;
function cpRender() {
// 内蔵プリセット
var bl = document.getElementById('cpBuiltinList');
if (bl) {
bl.innerHTML = CP_BUILTIN.map(function(p) {
var ac = cpActiveId === p.id ? ' cp-active' : '';
return '<button class="cp-btn' + ac + '" onclick="cpApply(\'' + p.id + '\',\'b\')" title="' + p.desc + '">' + p.icon + ' ' + p.name + '</button>';
}).join('');
}
// ユーザープリセット
var ul = document.getElementById('cpUserList');
var uc = document.getElementById('cpUserCount');
if (uc) uc.textContent = cpUserPresets.length ? '(' + cpUserPresets.length + ')' : '';
if (ul) {
if (!cpUserPresets.length) {
ul.innerHTML = '<span style="font-size:10px;color:var(--text2);">まだ保存なし</span>';
} else {
ul.innerHTML = cpUserPresets.map(function(p, i) {
var ac = cpActiveId === ('u' + i) ? ' cp-active' : '';
return '<span style="display:inline-flex;gap:0;align-items:stretch;">' +
'<button class="cp-btn' + ac + '" onclick="cpApply(' + i + ',\'u\')" title="' + (p.desc||'') + '">' + (p.icon||'📌') + ' ' + p.name + '</button>' +
'<button onclick="cpDeleteUser(' + i + ')" title="削除" style="background:#130810;border:1px solid rgba(255,100,100,.3);border-left:none;' +
'color:#ff8888;padding:4px 6px;border-radius:0 20px 20px 0;cursor:pointer;font-size:9px;line-height:1;">×</button>' +
'</span>';
}).join('');
}
}
}
function cpApply(idOrIdx, src) {
var preset;
if (src === 'b') {
preset = CP_BUILTIN.filter(function(p){ return p.id === idOrIdx; })[0];
cpActiveId = idOrIdx;
} else {
preset = cpUserPresets[idOrIdx];
cpActiveId = 'u' + idOrIdx;
}
if (!preset) return;
// 選択タグを置換
selectedTags = preset.tags.map(function(t){ return {tag:t.tag, preset:t.preset}; });
negSelectedTags = (preset.negTags||[]).map(function(t){ return {tag:t.tag, preset:t.preset}; });
// 追加テキストがあれば反映
if (preset.pos) {
translatedText = preset.pos;
var el = document.getElementById('translatedOutput');
if (el) { el.textContent = preset.pos; el.className = 'output-area'; }
}
groupOrder = [];
renderTags(); renderSelZone(); renderNegSelZone(); updateAllBtn(); updateTagCount(); updateFinal();
// アクティブラベル更新
var lbl = document.getElementById('cpActiveLabel');
if (lbl) lbl.textContent = '▸ 適用中: ' + (preset.icon||'') + ' ' + preset.name;
cpRender();
// toast
var t = document.getElementById('wcToast');
if (t) {
t.textContent = (preset.icon||'📌') + ' ' + preset.name + ' を適用しました';
t.classList.add('show');
clearTimeout(window._wcToastTimer);
window._wcToastTimer = setTimeout(function(){ t.classList.remove('show'); }, 2000);
}
}
function cpSaveCurrent() {
var name = (document.getElementById('cpSaveName')||{}).value;
name = name ? name.trim() : '';
var icon = (document.getElementById('cpSaveIcon')||{}).value;
icon = icon ? icon.trim() : '📌';
if (!icon) icon = '📌';
if (!name) {
var t = document.getElementById('wcToast');
if (t){ t.textContent = '⚠ プリセット名を入力してください'; t.classList.add('show'); clearTimeout(window._wcToastTimer); window._wcToastTimer = setTimeout(function(){ t.classList.remove('show'); }, 2000); }
return;
}
if (!selectedTags.length) {
var t2 = document.getElementById('wcToast');
if (t2){ t2.textContent = '⚠ タグが選択されていません'; t2.classList.add('show'); clearTimeout(window._wcToastTimer); window._wcToastTimer = setTimeout(function(){ t2.classList.remove('show'); }, 2000); }
return;
}
var pos = getFinalPos ? getFinalPos() : '';
cpUserPresets.unshift({
name: name, icon: icon, desc: pos ? pos.slice(0,40) : '',
tags: selectedTags.map(function(t){ return {tag:t.tag, preset:t.preset}; }),
negTags: negSelectedTags.map(function(t){ return {tag:t.tag, preset:t.preset}; }),
pos: translatedText||'', neg: translatedNegText||''
});
document.getElementById('cpSaveName').value = '';
try { localStorage.setItem('pf_char_presets', JSON.stringify(cpUserPresets)); } catch(e) {}
cpRender();
var t3 = document.getElementById('wcToast');
if (t3){ t3.textContent = '💾 「' + name + '」を保存しました'; t3.classList.add('show'); clearTimeout(window._wcToastTimer); window._wcToastTimer = setTimeout(function(){ t3.classList.remove('show'); }, 2000); }
}
function cpDeleteUser(i) {
if (!confirm('「' + cpUserPresets[i].name + '」を削除しますか?')) return;
if (cpActiveId === ('u' + i)) { cpActiveId = null; var lbl = document.getElementById('cpActiveLabel'); if(lbl) lbl.textContent=''; }
cpUserPresets.splice(i, 1);
try { localStorage.setItem('pf_char_presets', JSON.stringify(cpUserPresets)); } catch(e) {}
cpRender();
}
// ──────────────────────────────────────────────
// 埋め込みモードFooocus アコーディオン内 iframe
// ──────────────────────────────────────────────
(function() {
var _pfIsEmbed;
try { _pfIsEmbed = window.self !== window.top; } catch(e) { _pfIsEmbed = true; }
if (!_pfIsEmbed) return;
function buildEmbedBar() {
// すでに存在する場合はスキップ
if (document.getElementById('pf_embed_bar')) return;
var bar = document.createElement('div');
bar.id = 'pf_embed_bar';
bar.style.cssText = [
'position:fixed;bottom:0;left:0;right:0;z-index:99998',
'background:#0d0d1a;border-top:2px solid #7c4dff',
'padding:7px 12px;display:flex;gap:8px;align-items:center',
'box-shadow:0 -6px 24px rgba(124,77,255,.25)'
].join(';');
bar.innerHTML =
// Fooocusモードバッジ
'<span style="font-family:monospace;font-size:9px;letter-spacing:1px;' +
'color:#7c4dff;border:1px solid rgba(124,77,255,.4);padding:2px 6px;' +
'border-radius:3px;white-space:nowrap;flex-shrink:0;">FOOOCUS</span>' +
// プロンプトプレビュー
'<span id="pf_eb_preview" style="flex:1;min-width:0;color:#9999cc;font-size:11px;' +
'font-family:JetBrains Mono,monospace;overflow:hidden;text-overflow:ellipsis;' +
'white-space:nowrap;cursor:default;" title="作成中のプロンプト">' +
'タグを選択するとここに表示されます</span>' +
// 転送のみボタン
'<button id="pf_eb_send" onclick="pfSend(false)" ' +
'style="background:#1e1e35;border:1px solid #7c4dff;color:#c0c0ff;' +
'padding:7px 14px;border-radius:6px;cursor:pointer;font-size:12px;' +
'white-space:nowrap;flex-shrink:0;transition:background .15s;"' +
' onmouseover="this.style.background=\'#2a2a50\'"' +
' onmouseout="this.style.background=\'#1e1e35\'">' +
'→ 転送のみ</button>' +
// 転送して生成ボタン
'<button id="pf_eb_gen" onclick="pfSend(true)" ' +
'style="background:linear-gradient(90deg,#7c4dff,#e040fb);border:none;' +
'color:#fff;padding:7px 16px;border-radius:6px;cursor:pointer;' +
'font-size:13px;font-weight:bold;white-space:nowrap;flex-shrink:0;' +
'box-shadow:0 2px 12px rgba(124,77,255,.4);transition:opacity .15s;"' +
' onmouseover="this.style.opacity=\'0.9\'"' +
' onmouseout="this.style.opacity=\'1\'">' +
'⚡ 転送して生成</button>';
document.body.appendChild(bar);
// ページ内コンテンツがバーに隠れないようにpadding確保
document.body.style.paddingBottom = '50px';
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', buildEmbedBar);
} else {
buildEmbedBar();
}
// プロンプト更新時にプレビューを最新化updateFinal()から呼ばれる)
window._pfUpdateEmbedPreview = function() {
var el = document.getElementById('pf_eb_preview');
if (!el) return;
var pos = (typeof getFinalPos === 'function') ? getFinalPos() : '';
if (pos) {
el.textContent = pos.length > 90 ? pos.slice(0, 90) + '\u2026' : pos;
el.style.color = '#aaffcc';
el.title = pos;
} else {
el.textContent = 'タグを選択するとここに表示されます';
el.style.color = '#9999cc';
el.title = '作成中のプロンプト';
}
};
})();
function pfSend(andGenerate) {
var pos = (typeof getFinalPos === 'function') ? getFinalPos() : '';
var neg = (typeof getFinalNeg === 'function') ? getFinalNeg() : '';
if (!pos && !neg) {
// プロンプトが空の場合は警告
var bar = document.getElementById('pf_embed_bar');
if (bar) {
var prev = document.getElementById('pf_eb_preview');
if (prev) {
prev.style.color = '#ff6666';
prev.textContent = '⚠ プロンプトが空です。タグを選択してください。';
setTimeout(function() { window._pfUpdateEmbedPreview && window._pfUpdateEmbedPreview(); }, 2500);
}
}
return;
}
// postMessage で親ページFooocus Gradioへ送信
window.parent.postMessage({type: 'pf_send', pos: pos, neg: neg, generate: andGenerate}, '*');
// ボタンのビジュアルフィードバック
var id = andGenerate ? 'pf_eb_gen' : 'pf_eb_send';
var btn = document.getElementById(id);
if (btn) {
btn.disabled = true;
var orig = btn.innerHTML;
btn.innerHTML = andGenerate ? '✓ 転送完了!生成中...' : '✓ 転送しました!';
btn.style.opacity = '0.7';
setTimeout(function() {
btn.innerHTML = orig;
btn.style.opacity = '1';
btn.disabled = false;
}, 2500);
}
}
</script>
<div class="wc-toast" id="wcToast"></div>
<!-- ===== 色調補正モーダル ===== -->
<div class="color-modal-overlay" id="colorModalOverlay" onclick="if(event.target===this)closeColorModal()">
<div class="color-modal" role="dialog" aria-modal="true" aria-labelledby="colorModalTitle">
<button class="color-modal-close" onclick="closeColorModal()" aria-label="閉じる"></button>
<div class="color-modal-title" id="colorModalTitle">🎨 色調補正</div>
<div class="color-preview-img">
<img id="colorPreviewImg" src="" alt="補正プレビュー">
</div>
<div class="color-presets">
<span style="font-size:9px;color:var(--text2);font-family:'Orbitron',monospace;letter-spacing:1px;align-self:center;">PRESET:</span>
<button class="color-preset-btn active" onclick="applyColorPreset('normal',this)">🔄 オリジナル</button>
<button class="color-preset-btn" onclick="applyColorPreset('vivid',this)">✨ 鮮やか</button>
<button class="color-preset-btn" onclick="applyColorPreset('cinema',this)">🎬 映画風</button>
<button class="color-preset-btn" onclick="applyColorPreset('anime',this)">🌸 アニメ風</button>
<button class="color-preset-btn" onclick="applyColorPreset('warm',this)">🌅 暖かみ</button>
<button class="color-preset-btn" onclick="applyColorPreset('cool',this)">❄ クール</button>
<button class="color-preset-btn" onclick="applyColorPreset('hdr',this)">⚡ HDR</button>
</div>
<div class="color-slider-group">
<div class="color-slider-label">
<span>🌟 明るさ</span><span class="color-slider-val" id="brightnessVal">100%</span>
</div>
<input type="range" class="color-slider brightness-sl" id="brightnessSlider"
min="0" max="200" value="100" step="1"
oninput="updateColorFilter()" aria-label="明るさ" aria-valuemin="0" aria-valuemax="200">
</div>
<div class="color-slider-group">
<div class="color-slider-label">
<span>🎭 コントラスト</span><span class="color-slider-val" id="contrastVal">100%</span>
</div>
<input type="range" class="color-slider contrast-sl" id="contrastSlider"
min="0" max="200" value="100" step="1"
oninput="updateColorFilter()" aria-label="コントラスト" aria-valuemin="0" aria-valuemax="200">
</div>
<div class="color-slider-group">
<div class="color-slider-label">
<span>🎨 彩度</span><span class="color-slider-val" id="saturationVal">100%</span>
</div>
<input type="range" class="color-slider saturation-sl" id="saturationSlider"
min="0" max="300" value="100" step="1"
oninput="updateColorFilter()" aria-label="彩度" aria-valuemin="0" aria-valuemax="300">
</div>
<div class="color-slider-group">
<div class="color-slider-label">
<span>🌈 色相シフト</span><span class="color-slider-val" id="hueVal"></span>
</div>
<input type="range" class="color-slider hue-sl" id="hueSlider"
min="-180" max="180" value="0" step="1"
oninput="updateColorFilter()" aria-label="色相シフト" aria-valuemin="-180" aria-valuemax="180">
</div>
<div class="color-slider-group">
<div class="color-slider-label">
<span>🕰 セピア</span><span class="color-slider-val" id="sepiaVal">0%</span>
</div>
<input type="range" class="color-slider sepia-sl" id="sepiaSlider"
min="0" max="100" value="0" step="1"
oninput="updateColorFilter()" aria-label="セピア" aria-valuemin="0" aria-valuemax="100">
</div>
<div style="font-size:9px;color:var(--text2);font-family:'Noto Sans JP',sans-serif;margin-top:8px;margin-bottom:2px;">
💡 スライダーをダブルクリックすると個別リセット
</div>
<div class="color-modal-footer">
<button class="btn btn-c" onclick="downloadCorrectedImage()" style="flex:2;">⬇ 補正済みDL</button>
<button class="btn" onclick="saveColorToGallery()" style="flex:2;border-color:var(--success);color:var(--success);background:rgba(0,229,160,.08);"> ギャラリーに保存</button>
<button class="btn" onclick="sendToXFromColor()" style="flex:2;border-color:#1da1f2;color:#1da1f2;background:rgba(29,161,242,.08);">𝕏 投稿準備</button>
<button class="btn btn-d" onclick="resetColorFilter()" style="flex:1;">↺ リセット</button>
<button class="btn" onclick="closeColorModal()" style="flex:1;">✕ 閉じる</button>
</div>
</div>
</div>
<!-- ===== X投稿モーダル ===== -->
<div class="x-modal-overlay" id="xModalOverlay" onclick="if(event.target===this)closeXModal()">
<div class="x-modal" role="dialog" aria-modal="true" aria-labelledby="xModalTitle">
<button class="x-modal-close" onclick="closeXModal()" aria-label="閉じる"></button>
<div class="x-modal-title" id="xModalTitle">𝕏 X (Twitter) に投稿</div>
<img id="xPreviewThumb" class="x-preview-thumb" src="" alt="投稿画像プレビュー">
<div style="font-size:10px;color:var(--text2);margin-bottom:6px;font-family:'Noto Sans JP',sans-serif;">
📌 投稿キャプション280文字以内
</div>
<textarea class="x-caption-area" id="xCaption"
placeholder="キャプションを入力... (プロンプトから自動生成されます)"
oninput="updateXCharCount()"></textarea>
<div class="x-char-count" id="xCharCount">0 / 280</div>
<div style="font-size:10px;color:var(--text2);margin-top:10px;margin-bottom:4px;font-family:'Noto Sans JP',sans-serif;">
🏷 ハッシュタグ(クリックで追加/解除)
</div>
<div class="x-hashtag-row" id="xHashtagRow">
<button class="x-hashtag-btn" onclick="toggleHashtag(this,'#AIart')">#AIart</button>
<button class="x-hashtag-btn" onclick="toggleHashtag(this,'#AI生成')">#AI生成</button>
<button class="x-hashtag-btn" onclick="toggleHashtag(this,'#Fooocus')">#Fooocus</button>
<button class="x-hashtag-btn" onclick="toggleHashtag(this,'#AIイラスト')">#AIイラスト</button>
<button class="x-hashtag-btn" onclick="toggleHashtag(this,'#StableDiffusion')">#StableDiffusion</button>
<button class="x-hashtag-btn" onclick="toggleHashtag(this,'#画像生成AI')">#画像生成AI</button>
<button class="x-hashtag-btn" onclick="toggleHashtag(this,'#SDXL')">#SDXL</button>
<button class="x-hashtag-btn" onclick="toggleHashtag(this,'#AIgirl')">#AIgirl</button>
</div>
<div class="x-info-box">
<div style="font-size:9px;color:rgba(29,161,242,.8);font-family:'Orbitron',monospace;letter-spacing:1px;margin-bottom:6px;">📋 投稿手順</div>
<div style="font-size:10px;color:var(--text2);line-height:1.9;font-family:'Noto Sans JP',sans-serif;">
① 画像を自動ダウンロード<br>
② キャプション+ハッシュタグをクリップボードにコピー<br>
③ X.com が新しいタブで開きます<br>
④ 画像を添付 → キャプションを貼り付けて投稿
</div>
</div>
<div style="display:flex;gap:8px;margin-top:14px;flex-wrap:wrap;">
<button class="btn" id="xPostBtn" onclick="executeXPost()"
style="flex:3;padding:10px;border-color:#1da1f2;color:#1da1f2;background:rgba(29,161,242,.1);font-size:10px;letter-spacing:1px;">
𝕏 DL → コピー → X を開く
</button>
<button class="btn btn-d" onclick="closeXModal()" style="flex:1;">閉じる</button>
</div>
<div id="xPostMsg" style="display:none;margin-top:8px;font-size:11px;font-family:'Noto Sans JP',sans-serif;padding:8px;border-radius:6px;"></div>
</div>
</div>
</body>
</html>