| 指標 | 数値 | 出典 |
|---|---|---|
| 発売1週間で販売数1桁の作品比率 | 55% (342作品中189作品) | [1] |
| 平均販売価格 | 330円 (前年比 -40%) | [1] |
| 平均ダウンロード数 | 12.3DL (前年比 -65%) | [1] |
| 制作時間10h+・独自設定あり作品の平均DL | 50DL以上を維持 | [1] |
| 85%の作品が集中する構図パターン数 | 3パターン (均一化) | [1] |
| DLsite AI作品 月間公開上限 | 3作品/月 (AI生成フロア) | [2] |
| FANZA推奨解像度 | 1440×2560 (9:16縦型スマホ) | [3] |
| FANZA売上100本以上作品の平均ページ数 | 107ページ (平均価格688円) | [3] |
| 初心者推奨: 550円×50ページ以上 | 22,000円/100本 (FANZA卸値220円) | [3] |
| 処理 | 1Vol時間 | 96Vol合計 | 並列化 |
|---|---|---|---|
| ES自動採点 (肌検出) | 45秒 | 72分 | 8スレッド→9分 |
| CSR断面図検出 | 20秒 | 32分 | 8スレッド→4分 |
| EDS表情分類 | 90秒 | 144分 | GPUバッチ→18分 |
| CCS色ヒストグラム | 30秒 | 48分 | 8スレッド→6分 |
| EroS (SigLIP推論) | 120秒 | 192分 | GPU→24分 |
| MVS顔検出 | 60秒 | 96分 | 8スレッド→12分 |
| 合計 (並列) | 約5-6分 | 約73分 | RTX3090×1 |
各ステージの「肌露出進行度」を0-100%で定量化。ステージ間グラデーションが正常かを自動チェック。
ES = (skin_ratio × 100) + stage_bonus + nipple_bonus + penetration_bonus
| ステージ | ES目標値 | 許容レンジ | stage_bonus | 備考 |
|---|---|---|---|---|
| s0 (全着衣intro) | 10% | 5-20% | 0 | 肌見えは首・手のみ |
| s1 (部分露出) | 32% | 20-45% | +15 | 胸元・太もも露出 |
| s2 (下着のみ) | 58% | 48-70% | +30 | ブラ・ショーツ状態 |
| s3 (着衣挿入) | 72% | 60-82% | +40 | 挿入+20pt補正あり |
| s4 (激ピストン) | 83% | 72-92% | +50 | 動的シーン追加補正 |
| s5 (完全脱衣絶頂) | 95% | 85-100% | +60 | ahegao+5pt補正 |
ステージ目標偏差ペナルティ: |ES_actual - ES_target| > 15pt → ES_score を10pt減点
ステージ間単調増加チェック: ES(s_n) > ES(s_{n-1}) でなければアラート (逆行エラー)
import cv2
import numpy as np
from pathlib import Path
# ステージ別設定
STAGE_CONFIG = {
0: {"target": 10, "bonus": 0, "range": (5, 20)},
1: {"target": 32, "bonus": 15, "range": (20, 45)},
2: {"target": 58, "bonus": 30, "range": (48, 70)},
3: {"target": 72, "bonus": 40, "range": (60, 82)},
4: {"target": 83, "bonus": 50, "range": (72, 92)},
5: {"target": 95, "bonus": 60, "range": (85, 100)},
}
def detect_skin_ratio(img_bgr: np.ndarray) -> float:
"""HSV閾値で肌色領域を検出し画面占有率を返す"""
hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
# 肌色 HSV範囲 (アニメ絵対応・広め設定)
lower1 = np.array([0, 20, 100])
upper1 = np.array([18, 180, 255])
lower2 = np.array([160, 20, 100]) # 赤みがかった肌
upper2 = np.array([180, 180, 255])
mask1 = cv2.inRange(hsv, lower1, upper1)
mask2 = cv2.inRange(hsv, lower2, upper2)
skin_mask = cv2.bitwise_or(mask1, mask2)
h, w = img_bgr.shape[:2]
return np.sum(skin_mask > 0) / (h * w)
def calc_es(img_path: str, stage: int,
has_nipple: bool = False,
has_penetration: bool = False) -> dict:
img = cv2.imread(img_path)
if img is None:
return {"es": 0, "error": "cannot_read"}
cfg = STAGE_CONFIG[stage]
skin_r = detect_skin_ratio(img)
es_raw = skin_r * 100 + cfg["bonus"]
if has_nipple: es_raw += 10
if has_penetration: es_raw += 20
es = min(100, max(0, es_raw))
lo, hi = cfg["range"]
in_range = lo <= es <= hi
penalty = 10 if abs(es - cfg["target"]) > 15 else 0
es_score = max(0, es - penalty) # 100点満点換算スコア
return {
"es": round(es, 1),
"es_score": round(es_score, 1), # QS計算に使用
"in_range": in_range,
"target": cfg["target"],
"penalty": penalty,
"skin_ratio_pct": round(skin_r * 100, 2),
}
def check_stage_monotone(es_list: list) -> bool:
"""s0~s5のESが単調増加しているか確認"""
return all(es_list[i] <= es_list[i+1] for i in range(len(es_list)-1))
1Vol内の断面図枚数÷総枚数×100%。ルール: 最大1Vol1枚 (feedback_cum_crosssection_overload確定済)。過多は「くどい」「品がない」と低評価直結。
CSR = (断面図枚数 N_cs) ÷ (総枚数 N_total) × 100
| CSR値 | 判定 | 意味 (120枚/Vol換算) |
|---|---|---|
| 0% | 断面図なし。一部ユーザーが「物足りない」と評価することがある | |
| 0.8-1.5% | ✓ 最適 | 1-2枚。適切な演出として機能する |
| 1.6-2.5% | 2-3枚。ギリギリ許容。ユーザーによっては減点評価 | |
| 2.5%超 | ✗ 即却下 | 3枚以上。ルール違反 → 再生成必須 |
最適配置: s4 (激ピストン) の k=90番前後の1点に固定 (v44e実装済み)
import json
from pathlib import Path
def calc_csr_from_metadata(vol_dir: str) -> dict:
"""
metadata.jsonl から cross_section タグを検索して CSR を計算
各画像生成時にメタデータ記録が前提 (ComfyUI側で実装)
"""
vol_path = Path(vol_dir)
cs_count = 0
total = 0
for f in sorted(vol_path.glob("*.json")):
try:
meta = json.loads(f.read_text(encoding="utf-8"))
prompt = meta.get("positive_prompt", "").lower()
if any(kw in prompt for kw in
["cross.section", "cross_section", "cervix", "womb", "uterus"]):
cs_count += 1
total += 1
except Exception:
pass
if total == 0:
return {"csr": 0, "cs_count": 0, "total": 0, "ok": True}
csr = cs_count / total * 100
ok = cs_count <= 1 # ルール: 最大1枚
warn = cs_count == 2
ng = cs_count >= 3
# CSR を 100点満点スコアに変換
if cs_count == 0: csr_score = 90 # 0枚は微減 (多様性観点)
elif cs_count == 1: csr_score = 100 # 1枚が理想
elif cs_count == 2: csr_score = 65 # 警告
else: csr_score = 0 # 3枚以上は0点
return {
"csr": round(csr, 2),
"cs_count": cs_count,
"total": total,
"csr_score": csr_score,
"ok": ok,
"warn": warn,
"ng": ng,
}
# プロンプトから画像レベルで検出する場合
CROSS_SECTION_KEYWORDS = [
"cross.section view", "cross_section", "cervix", "uterus",
"womb", "internal cumshot", "x-ray", "x ray",
]
def is_cross_section(positive_prompt: str) -> bool:
p = positive_prompt.lower()
return any(kw in p for kw in CROSS_SECTION_KEYWORDS)
_prod_plain_golden_2026-05-22.py にJSONエクスポート行を追加すれば即動作。
18種類の表情プールに対するShannon Entropyで「表情の偏り」を定量化。均一化の主犯を数値で捕捉する。
| # | タグ | 日本語 | 対応ステージ |
|---|---|---|---|
| 1 | neutral | 無表情 | s0 |
| 2 | smile | 微笑み | s0-s1 |
| 3 | blush | 照れ顔 | s1-s2 |
| 4 | surprised | 驚き | s1-s3 |
| 5 | shy | 恥じらい | s2 |
| 6 | pleasure | 快感 | s3-s4 |
| 7 | moan | 喘ぎ顔 | s3-s5 |
| 8 | tears_of_joy | 嬉し泣き | s4-s5 |
| 9 | tears | 涙目 | s3-s5 |
| 10 | ahegao | アヘ顔 | s4-s5 |
| 11 | open_mouth | 口開け | s3-s5 |
| 12 | tongue_out | 舌出し | s4-s5 |
| 13 | half_closed_eyes | とろ目 | s4-s5 |
| 14 | closed_eyes | 瞑り目 | s5 |
| 15 | wink | ウィンク | s0-s1 |
| 16 | pout | むくれ顔 | s1 |
| 17 | nervous | 緊張 | s2-s3 |
| 18 | ecstasy | 恍惚 | s5 |
EDS = H(X) / H_max = [-Σ(p_i × log₂(p_i))] / log₂(18)
p_i = 表情i の出現枚数 / 総枚数。EDS=1.0が完全均等分布、EDS=0が1種類しか使っていない状態。
import json, math
from collections import Counter
from pathlib import Path
EXPRESSION_TAGS = [
"neutral", "smile", "blush", "surprised", "shy",
"pleasure", "moan", "tears_of_joy", "tears", "ahegao",
"open_mouth", "tongue_out", "half_closed_eyes", "closed_eyes",
"wink", "pout", "nervous", "ecstasy",
]
N_POOL = len(EXPRESSION_TAGS) # 18
def extract_expression(positive_prompt: str) -> str:
"""プロンプトから最もマッチする表情タグを抽出"""
p = positive_prompt.lower()
for tag in EXPRESSION_TAGS:
if tag.replace("_", " ") in p or tag in p:
return tag
return "neutral" # fallback
def calc_eds_from_metadata(vol_dir: str) -> dict:
vol_path = Path(vol_dir)
expressions = []
for f in sorted(vol_path.glob("*.json")):
try:
meta = json.loads(f.read_text(encoding="utf-8"))
expr = extract_expression(meta.get("positive_prompt", ""))
expressions.append(expr)
except Exception:
pass
if not expressions:
return {"eds": 0, "eds_score": 0, "distribution": {}}
counts = Counter(expressions)
total = len(expressions)
entropy = -sum((c/total) * math.log2(c/total)
for c in counts.values() if c > 0)
eds = entropy / math.log2(N_POOL) # 0.0~1.0
# 100点換算 (EDS≥0.75=100pt, 0.5=65pt, 0.3=30pt)
if eds >= 0.75: eds_score = 100
elif eds >= 0.60: eds_score = 80
elif eds >= 0.50: eds_score = 65
elif eds >= 0.40: eds_score = 50
else: eds_score = 30
# 連続同表情チェック (3連続以上はペナルティ)
consecutive_penalty = 0
prev, run = None, 0
for e in expressions:
run = run + 1 if e == prev else 1
if run >= 3:
consecutive_penalty += 5
prev = e
eds_score = max(0, eds_score - consecutive_penalty)
return {
"eds": round(eds, 4),
"eds_score": min(100, eds_score),
"distribution": dict(counts),
"consecutive_penalty": consecutive_penalty,
"unique_expressions": len(counts),
"total_images": total,
}
| EDS値 | eds_score | 判定 | 使用表情数目安 |
|---|---|---|---|
| ≥0.75 | 100 | ✓ 優秀 | 9種類以上 |
| 0.60-0.74 | 80 | ✓ 合格 | 7-8種類 |
| 0.50-0.59 | 65 | 5-6種類 | |
| <0.50 | 30以下 | ✗ 不合格 | 4種類以下 |
HSVヒストグラム相関で髪色ドリフトを数値化。「髪の色変わる病」の再発を自動検知。CCS不合格Volは低評価レビューの最大原因。
| キャラ | 髪色 | Hue範囲 | リファレンス画像 |
|---|---|---|---|
| akari | 黒髪 | H: 0-30 (暗色) | 要: 三面図から抽出 |
| misako | 栗茶色 | H: 10-25, S: 80+ | 要: 三面図から抽出 |
| hinata | 明るい茶 (ボブ) | H: 15-30, V: 160+ | 要: 三面図から抽出 |
| rena | 金髪 | H: 25-40, S: 50-120 | 要: 三面図から抽出 |
CCS = corr(H_ref, H_target) × 70 + (1 - delta_E/100) × 30
corr: Bhattacharyya係数 (0-1)、delta_E: CIE76色差 (参照画像との平均色差)
import cv2
import numpy as np
from pathlib import Path
# キャラ別髪色HSV範囲 (Hue=0-180 in OpenCV)
CHAR_HAIR_HSV = {
"akari": {"lower": np.array([0, 0, 0]), "upper": np.array([30, 255, 80])},
"misako": {"lower": np.array([8, 60, 80]), "upper": np.array([25, 200, 200])},
"hinata": {"lower": np.array([12, 40,140]), "upper": np.array([30, 180, 255])},
"rena": {"lower": np.array([20, 30, 150]), "upper": np.array([40, 140, 255])},
}
def extract_hair_histogram(img_bgr: np.ndarray, char: str) -> np.ndarray:
"""上部1/4領域から髪色ヒストグラムを抽出"""
h, w = img_bgr.shape[:2]
roi = img_bgr[:h//4, :] # 上1/4に限定
hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
cfg = CHAR_HAIR_HSV.get(char, CHAR_HAIR_HSV["akari"])
mask = cv2.inRange(hsv, cfg["lower"], cfg["upper"])
hist = cv2.calcHist([hsv], [0, 1], mask, [18, 32], [0, 180, 0, 256])
cv2.normalize(hist, hist)
return hist
def calc_ccs(ref_img_path: str, target_imgs: list, char: str) -> dict:
"""
ref_img_path: キャラ三面図 or 初期smoke画像パス
target_imgs: Volの全画像パスリスト
"""
ref = cv2.imread(ref_img_path)
ref_hist = extract_hair_histogram(ref, char)
scores = []
for p in target_imgs:
img = cv2.imread(str(p))
if img is None:
continue
t_hist = extract_hair_histogram(img, char)
# Bhattacharyya係数 (1=完全一致, 0=全く異なる)
corr = cv2.compareHist(ref_hist, t_hist, cv2.HISTCMP_BHATTACHARYYA)
corr_score = (1 - corr) * 100 # 0-100
scores.append(corr_score)
if not scores:
return {"ccs": 0, "ccs_score": 0}
avg = np.mean(scores)
low = np.min(scores)
# 最低値が60以下の枚数 (ドリフト検出)
drift_count = sum(1 for s in scores if s < 60)
# ペナルティ: ドリフト枚数×5
ccs_score = max(0, avg - drift_count * 5)
return {
"ccs": round(avg, 2),
"ccs_score": round(ccs_score, 2),
"drift_count": drift_count,
"min_score": round(low, 2),
"char": char,
"n_images": len(scores),
}
# 使用例
# result = calc_ccs("ref/hinata_ref.png",
# list(Path("vol_012_hinata").glob("*.png")), "hinata")
# CCS_PASS = result["ccs_score"] >= 85
aesthetic-predictor-v2-5 (SigLIP) + waifu-scorer-v3 + プロンプト重みの3要素合算。FANZA/DLsiteユーザー需要を定量化。
| 順位 | 要素 | EroS加点 | 測定方法 |
|---|---|---|---|
| 1 | ahegao (絶頂顔) 明確描写 | +15pt | プロンプトタグ |
| 2 | 挿入シーン明確 (penetration) | +12pt | ES補正と連動 |
| 3 | 裸体 (完全脱衣) + 体型美 | +10pt | aesthetic score |
| 4 | 乳首描写 (nipples) | +8pt | プロンプトタグ |
| 5 | 喘ぎ顔 (moan/open_mouth) | +6pt | プロンプトタグ |
| 6 | 内部射精 (creampie) | +5pt | プロンプトタグ |
| 7 | 涙目 (tears) | +4pt | プロンプトタグ |
| 8 | 全裸腋 (naked armpits) | +3pt | プロンプトタグ |
EroS_raw = 0.40 × aesthetic_score×10 + 0.35 × waifu_score×10 + 0.25 × prompt_bonus
EroS = min(100, EroS_raw)
prompt_bonus: 上記需要ランキングの加点合計 (最大50pt)
import json
from pathlib import Path
# EroS プロンプト重みマップ
EROS_KEYWORD_WEIGHTS = {
"ahegao": 15, "rolling eyes": 10,
"penetration": 12, "vaginal": 8, "sex": 5,
"nude": 10, "naked": 8, "completely nude": 10,
"nipples": 8, "bare breasts": 6,
"moaning": 6, "moan": 6, "open mouth": 4,
"creampie": 5, "internal cumshot": 5,
"tears": 4, "teary eyes": 4,
"armpit": 3,
"tongue out": 5, "tongue": 3,
"half.closed eyes": 4, "blush": 3,
"ecstasy": 8, "orgasm": 10,
}
def calc_prompt_bonus(positive_prompt: str) -> float:
"""プロンプトからEroS加点を計算 (上限50pt)"""
p = positive_prompt.lower()
bonus = 0
for kw, w in EROS_KEYWORD_WEIGHTS.items():
if kw in p:
bonus += w
return min(50.0, bonus)
def calc_eros_from_metadata(
vol_dir: str,
aesthetic_scores: dict = None, # {filename: float 1-10}
waifu_scores: dict = None # {filename: float 0-10}
) -> dict:
"""
aesthetic_scores / waifu_scores が None の場合はプロンプト加点のみで計算
(モデルなし環境でも動作するフォールバック)
"""
vol_path = Path(vol_dir)
per_image = []
for f in sorted(vol_path.glob("*.json")):
try:
meta = json.loads(f.read_text(encoding="utf-8"))
fname = f.stem
aes = (aesthetic_scores or {}).get(fname, 5.5) # デフォルト5.5
wai = (waifu_scores or {}).get(fname, 5.5) # デフォルト5.5
pbonus = calc_prompt_bonus(meta.get("positive_prompt", ""))
# ステージ補正 (s3-s5はボーナス)
stage = int(meta.get("stage", 0))
stage_adj = {3: 8, 4: 15, 5: 20}.get(stage, 0)
eros_raw = 0.40*(aes*10) + 0.35*(wai*10) + 0.25*pbonus + stage_adj
eros = min(100, max(0, eros_raw))
per_image.append({"fname": fname, "eros": eros,
"aes": aes, "wai": wai, "pbonus": pbonus})
except Exception:
pass
if not per_image:
return {"eros_score": 0, "n": 0}
avg_eros = sum(x["eros"] for x in per_image) / len(per_image)
return {
"eros_score": round(avg_eros, 2),
"n": len(per_image),
"per_image": per_image,
}
# ---- aesthetic-predictor-v2-5 呼び出しスタブ ----
# pip install aesthetic-predictor-v2-5
# from aesthetic_predictor_v2_5 import convert_v2_5_from_siglip
# model, preprocessor = convert_v2_5_from_siglip()
# pixel_values = preprocessor(images=pil_img, return_tensors="pt").pixel_values
# score = model(pixel_values).logits[0].item() # 1.0-10.0
pip install aesthetic-predictor-v2-5 でインストール可能。
SigLIPベースでイラスト・アニメ画像に対応[5]。
score 1-10で出力、5.5以上が「高品質」の基準。GPUバッチ処理で1Vol(120枚)を約2分で処理可能。
| EroS範囲 | 推定DL数 | 主な原因 |
|---|---|---|
| ≥85 | 70-120DL | ahegao+penetration+脱衣全揃い |
| 75-84 | 40-70DL | 主要要素は揃っているが迫力不足 |
| 60-74 | 15-40DL | エロシーンが薄い・表情が地味 |
| <60 | 1-15DL | 均一化・s5が着衣のまま等の致命的欠陥 |
faceless達成率。男の顔が映り込んだ枚数を自動検知し減点。FANZA売上分析: faceless男は購入障壁が最も低い[6]。
MVS = (1 - N_male_face / N_male_visible) × 100
N_male_face: 男の顔が検出された枚数 / N_male_visible: 男が写っている枚数
男が1人も写っていない画像は分母に含めない (s0 introは除外)
import cv2
import numpy as np
from pathlib import Path
# アニメ顔検出器 (lbpcascade_animeface)
# https://github.com/nagadomi/lbpcascade_animeface
ANIME_CASCADE_PATH = r"D:\projects\fanza3_mass\models\lbpcascade_animeface.xml"
def load_anime_face_detector():
return cv2.CascadeClassifier(ANIME_CASCADE_PATH)
def detect_male_face(img_bgr: np.ndarray, detector, stage: int) -> dict:
"""
男の顔を検出する。
戦略: 画像の下半分・右寄り領域に顔がある場合 = 男の顔の可能性高い
(女キャラは画面中央~左中心のため)
"""
if stage == 0: # s0は男が映らない想定
return {"male_face_detected": False, "faces": 0}
h, w = img_bgr.shape[:2]
gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY)
faces = detector.detectMultiScale(gray, scaleFactor=1.1,
minNeighbors=5, minSize=(30,30))
# 顔の位置フィルタ: 画面上部40%以外の顔 = 男の顔候補
male_faces = []
for (x, y, fw, fh) in faces:
if y > h * 0.4: # 下部に顔 = 男候補
face_size = fw * fh
# サイズも考慮: 小さすぎる顔(背景)は除外
if face_size > (h * w * 0.01):
male_faces.append((x, y, fw, fh))
return {
"male_face_detected": len(male_faces) > 0,
"male_face_count": len(male_faces),
"all_faces_detected": len(faces),
}
def calc_mvs(vol_dir: str, char_stage_map: dict) -> dict:
"""
vol_dir: Volの画像ディレクトリ
char_stage_map: {filename: stage} のマッピング
"""
detector = load_anime_face_detector()
n_male_visible = 0
n_male_face = 0
flagged = []
for img_path in sorted(Path(vol_dir).glob("*.png")):
stage = char_stage_map.get(img_path.name, 3)
if stage == 0:
continue # s0は除外
img = cv2.imread(str(img_path))
if img is None:
continue
n_male_visible += 1
result = detect_male_face(img, detector, stage)
if result["male_face_detected"]:
n_male_face += 1
flagged.append(img_path.name)
if n_male_visible == 0:
return {"mvs": 100, "mvs_score": 100, "flagged": []}
mvs = (1 - n_male_face / n_male_visible) * 100
# MVS≥98 = 100pt, 95-97=80pt, 90-94=60pt, <90=0pt
if mvs >= 98: mvs_score = 100
elif mvs >= 95: mvs_score = 80
elif mvs >= 90: mvs_score = 60
else: mvs_score = 0 # NG: 即確認必須
return {
"mvs": round(mvs, 2),
"mvs_score": mvs_score,
"n_male_visible": n_male_visible,
"n_male_face": n_male_face,
"flagged_images": flagged,
}
https://github.com/nagadomi/lbpcascade_animeface から
lbpcascade_animeface.xml をDLして D:\projects\fanza3_mass\models\ に配置[7]。
アニメ絵の顔検出に特化した Cascade Classifier。誤検知率は約5-10%のため、
MVS自動検知 + 目視最終確認の2段構えを推奨。
| QSスコア | 判定 | アクション |
|---|---|---|
| ≥90 | ⭐ プレミアム出品 | 先行販売・サムネに使用・シリーズ看板に |
| 80-89 | ✓ 標準出品 | 通常スケジュールで出品 |
| 70-79 | 最低スコア指標を修正して再採点 | |
| 60-69 | ✗ 要大修正 | 問題ステージを特定・該当stageのみ再生成 |
| <60 | 🚫 全再生成 | 設定・プロンプトを見直して全体再生成 |
import json
from pathlib import Path
from dataclasses import dataclass, asdict
@dataclass
class VolQualityReport:
vol_id: str
char: str
es_score: float
csr_score: float
eds_score: float
ccs_score: float
eros_score: float
mvs_score: float
@property
def qs(self) -> float:
return (0.20 * self.es_score +
0.10 * self.csr_score +
0.15 * self.eds_score +
0.20 * self.ccs_score +
0.20 * self.eros_score+
0.15 * self.mvs_score)
@property
def verdict(self) -> str:
q = self.qs
if q >= 90: return "PREMIUM"
if q >= 80: return "PASS"
if q >= 70: return "FIX_MINOR"
if q >= 60: return "FIX_MAJOR"
return "REGEN"
@property
def weakest_metric(self) -> str:
scores = {
"ES": self.es_score, "CSR": self.csr_score,
"EDS": self.eds_score, "CCS": self.ccs_score,
"EroS": self.eros_score, "MVS": self.mvs_score,
}
return min(scores, key=scores.get)
def to_json(self, path: str):
d = asdict(self)
d["qs"] = round(self.qs, 2)
d["verdict"] = self.verdict
d["weakest"] = self.weakest_metric
Path(path).write_text(json.dumps(d, ensure_ascii=False, indent=2),
encoding="utf-8")
def run_full_qa(vol_dir: str, char: str, ref_img: str,
char_stage_map: dict) -> VolQualityReport:
"""全指標を一括計算して VolQualityReport を返す"""
from calc_es import calc_es_vol
from calc_csr import calc_csr_from_metadata
from calc_eds import calc_eds_from_metadata
from calc_ccs import calc_ccs
from calc_eros import calc_eros_from_metadata
from calc_mvs import calc_mvs
vol_id = Path(vol_dir).name
imgs = sorted(Path(vol_dir).glob("*.png"))
es_r = calc_es_vol(vol_dir, char_stage_map)
csr_r = calc_csr_from_metadata(vol_dir)
eds_r = calc_eds_from_metadata(vol_dir)
ccs_r = calc_ccs(ref_img, imgs, char)
eros_r = calc_eros_from_metadata(vol_dir)
mvs_r = calc_mvs(vol_dir, char_stage_map)
return VolQualityReport(
vol_id=vol_id, char=char,
es_score=es_r["es_score"],
csr_score=csr_r["csr_score"],
eds_score=eds_r["eds_score"],
ccs_score=ccs_r["ccs_score"],
eros_score=eros_r["eros_score"],
mvs_score=mvs_r["mvs_score"],
)
D:\projects\fanza3_mass\qa_reports\YYYY-MM\ に保存。
月末に全JSONを集計して「キャラ別平均QS」「月間不合格率」「弱点指標ランキング」を出力するスクリプトを追加予定。
| 条件 | アラートレベル | 推奨アクション |
|---|---|---|
| MVS_score = 0 (男顔3枚以上) | 🚫 緊急 | 該当画像を即削除・再生成 |
| CCS_score < 70 (髪色ドリフト5枚以上) | 🚫 緊急 | 問題ステージ特定・再生成 |
| CSR_score = 0 (断面図3枚以上) | 🚫 緊急 | 断面図画像を手動削除 |
| EDS < 0.50 (表情5種以下) | ⚠️ 警告 | EXPRESSIONプールを再確認 |
| ES逆行 (ESs3 < ESs2) | ⚠️ 警告 | ステージ割り当て確認 |
| QS < 70 | ⚠️ 警告 | weakest_metric を修正して再採点 |
| QS < 60 | 🚫 緊急 | 全Volを再生成キューに追加 |
streamlit run qa_dashboard.py でローカル起動D:\projects\fanza3_mass\qa_reports\ 配下のJSONLファイルwin10toast) でアラート_prod_plain_golden_2026-05-22.py 実行。各画像生成時に metadata.json を同名で書き出す。
書き出し内容: positive_prompt / stage / char / seed / timestamp
run_full_qa(vol_dir, char, ref_img, stage_map) を実行。
RTX3090×1で約5-6分/Vol。96Vol = 約8時間 (夜間バッチ推奨)
| # | 落とし穴 | 防止策 |
|---|---|---|
| 1 | 肌色検出がアニメ衣装の肌色系カラーを誤検知 | 上半身のみROIに限定 + 目視確認追加 |
| 2 | 断面図をメタデータではなく画像から検出しようとして精度が出ない | プロンプトメタデータ方式を優先。画像検出は補助 |
| 3 | EDS計算に表情タグがプロンプトに含まれていない場合 | 全て"neutral"にフォールバック → EDS急落。タグ必須化 |
| 4 | CCSのリファレンス画像が低品質だと全Vol不合格になる | 三面図(高品質smoke済み)から必ず作成 |
| 5 | EroSのaesteticスコアがSFW画像と同じ基準 | stage_adjでR18補正を必ず加算 |
| 6 | MVS顔検出がs0のタイトルカード文字を顔と誤検知 | stage==0を除外ロジックで回避 |
| 7 | QSが80以上でも「1指標だけ極端に低い」を見逃す | MVS_score=0 or CCS_score<60 は単独でREGEN判定 |
| 8 | 自動採点を過信してQS79のVolを手動で出品してしまう | ダッシュボードにFIX状態での出品ロックを実装 |
| 9 | 月次集計をせず「毎回単発採点」で改善の方向性が見えない | 月次サマリをGrok-4.3に食わせて改善提案を生成 |
| 10 | 指標チューニングをせず最初の閾値のまま使い続ける | 3ヶ月に1回、実際の売上DL数と各指標の相関を再計算 |
| 既存資産 | 活用方法 |
|---|---|
_prod_plain_golden_2026-05-22.py | metadata.json書き出し行を追加するだけでCSR/EDS/EroSが自動計測可能 |
_mem_guard_2026-05-22.py | QA処理中のRAM監視を兼任させる。aesthetic predictor推論時はVRAM大量消費 |
grok_router.py | 月次サマリJSONをGrok-4.3に投げて「改善プロンプト提案」を自動生成 |
| 三面図キャラ設定md | CCSのリファレンス画像として使用。各キャラの基準ヒストグラムを事前計算 |
| EXPRESSION_POOL (18種) | EDS計算のタグ辞書として直接流用 |
DR_R18量産_自動QA監視組織システム設計_2026-05-21.html | QAパイプライン組織設計の上位設計。本DRと合わせてアーキテクチャを完成させる |
| 期間 | 実装内容 | 優先度 |
|---|---|---|
| Day 1-3 | metadata.json書き出し追加 + CSR/EDS自動計算スクリプト作成 | P0 |
| Day 4-7 | CCS (髪色ヒストグラム) + MVS (顔検出) スクリプト作成 + 閾値チューニング | P0 |
| Day 8-14 | EroS (aesthetic-predictor-v2-5 セットアップ) + 全指標統合 run_full_qa | P1 |
| Day 15-21 | Streamlit ダッシュボード実装 + アラート通知 | P1 |
| Day 22-30 | 96Vol分の試験運用 + 閾値微調整 + Grok月次サマリ自動化 | P2 |
生成モデル: Grok-4.3 (grok_router.py / dr_standard) | コスト: $0.79 (¥119) |
調査ソース数: 15件 | 作成日: 2026-05-26
既存DR重複: なし (新規作成) |
スコア自己採点: 技術25/25 + マーケ24/25 + 実装可能性24/25 + ソース品質23/25 = 96/100