Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afc7a37d17 | |||
| 731ad37217 | |||
| c551ecbcc5 | |||
| 873fa89305 | |||
| 225406cc2e | |||
| 4ccb395dd1 | |||
| f5162455c2 | |||
| 5b4b844f1a | |||
| 587eefd8fa | |||
| cbc9fc885b | |||
| 1fb156745d | |||
| 607e989b95 | |||
| c277e87ad3 | |||
| c25269051e | |||
| 54842c1387 | |||
| d1c137af8a | |||
| f0eaf4c31f | |||
| a8a20d6f87 | |||
| 0fa2397f33 |
@@ -0,0 +1,107 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""100: 修正已有 capability 的模型绑定.
|
||||
|
||||
幂等:仅当 primary_model_id 当前绑定到旧模型 (doubao-seed-1-6) 时才更新,
|
||||
避免覆盖用户在后台的自定义配置。
|
||||
|
||||
- 更新 5 个 LLM capability (intent_parsing, copy_fusion, storyboard, copy_review, asset_classify)
|
||||
的 primary_model_id 从 doubao-seed-1-6 改为 doubao-seed-2-1-pro-260915
|
||||
- 更新 image_analysis 的 primary/lite/fallback 模型绑定
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "100_fix_capability_model_bindings"
|
||||
down_revision = "099_ai_model_router_seed"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Check tables exist
|
||||
table_check = conn.execute(sa.text("SELECT to_regclass('public.ai_models')")).scalar()
|
||||
if not table_check:
|
||||
return
|
||||
|
||||
config_table_check = conn.execute(sa.text("SELECT to_regclass('public.ai_capability_configs')")).scalar()
|
||||
if not config_table_check:
|
||||
return
|
||||
|
||||
# Look up model IDs by model_key (not hardcoded UUIDs)
|
||||
pro_model_row = conn.execute(
|
||||
sa.text(
|
||||
"SELECT id FROM ai_models WHERE model_key = 'doubao-seed-2-1-pro-260915' AND deleted_at IS NULL LIMIT 1"
|
||||
)
|
||||
).first()
|
||||
if not pro_model_row:
|
||||
return
|
||||
pro_model_id = pro_model_row[0]
|
||||
|
||||
old_model_row = conn.execute(
|
||||
sa.text("SELECT id FROM ai_models WHERE model_key = 'doubao-seed-1-6-250615' LIMIT 1")
|
||||
).first()
|
||||
old_model_id = old_model_row[0] if old_model_row else None
|
||||
|
||||
llm_capabilities = [
|
||||
"intent_parsing",
|
||||
"copy_fusion",
|
||||
"storyboard",
|
||||
"copy_review",
|
||||
"asset_classify",
|
||||
]
|
||||
|
||||
for cap_key in llm_capabilities:
|
||||
if old_model_id:
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"UPDATE ai_capability_configs SET primary_model_id = :new_id, updated_at = NOW() "
|
||||
"WHERE capability_key = :cap_key AND primary_model_id = :old_id"
|
||||
),
|
||||
{"new_id": pro_model_id, "old_id": old_model_id, "cap_key": cap_key},
|
||||
)
|
||||
|
||||
# Update image_analysis
|
||||
qwen38_row = conn.execute(
|
||||
sa.text("SELECT id FROM ai_models WHERE model_key = 'qwen3.8-flash' AND deleted_at IS NULL LIMIT 1")
|
||||
).first()
|
||||
qwen37_row = conn.execute(
|
||||
sa.text("SELECT id FROM ai_models WHERE model_key = 'qwen3.7-plus' AND deleted_at IS NULL LIMIT 1")
|
||||
).first()
|
||||
|
||||
if qwen38_row and qwen37_row:
|
||||
qwen38_id = qwen38_row[0]
|
||||
qwen37_id = qwen37_row[0]
|
||||
|
||||
current_ia = conn.execute(
|
||||
sa.text(
|
||||
"SELECT primary_model_id, lite_model_id, fallback_model_id "
|
||||
"FROM ai_capability_configs WHERE capability_key = 'image_analysis'"
|
||||
)
|
||||
).first()
|
||||
|
||||
if current_ia:
|
||||
current_primary, current_lite, current_fallback = current_ia
|
||||
updates = {}
|
||||
if current_primary != qwen38_id:
|
||||
updates["primary_model_id"] = qwen38_id
|
||||
if current_lite != qwen38_id:
|
||||
updates["lite_model_id"] = qwen38_id
|
||||
if current_fallback != qwen37_id:
|
||||
updates["fallback_model_id"] = qwen37_id
|
||||
|
||||
if updates:
|
||||
set_clause = ", ".join([f"{k} = :{k}" for k in updates.keys()])
|
||||
set_clause += ", updated_at = NOW()"
|
||||
updates["cap_key"] = "image_analysis"
|
||||
conn.execute(
|
||||
sa.text(f"UPDATE ai_capability_configs SET {set_clause} WHERE capability_key = :cap_key"),
|
||||
updates,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,153 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""101: 补齐 qwen-vl-plus 视觉模型并修正 image_analysis 绑定与 max_tokens.
|
||||
|
||||
背景:
|
||||
- qwen-vl-plus 做图片识别时返回 JSON 约 500-600 tokens,旧硬编码
|
||||
max_tokens=350 导致 JSON 被截断、解析失败返回"未识别"。
|
||||
- 代码侧已移除硬编码,改由 capability 的 DB 配置决定 max_tokens。
|
||||
|
||||
幂等:
|
||||
- qwen-vl-plus 已存在则不插入;
|
||||
- 仅当 image_analysis 当前 primary_model 不是 qwen-vl-plus 时才更新绑定,
|
||||
避免覆盖后台手动配置。
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "101_qwen_vl_plus_and_max_tokens"
|
||||
down_revision = "100_fix_capability_model_bindings"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
models_table = conn.execute(sa.text("SELECT to_regclass('public.ai_models')")).scalar()
|
||||
if not models_table:
|
||||
return
|
||||
|
||||
caps_table = conn.execute(sa.text("SELECT to_regclass('public.ai_capability_configs')")).scalar()
|
||||
if not caps_table:
|
||||
return
|
||||
|
||||
# ── c. 补全其他 capability 的 max_tokens 默认值(幂等)──────────────────
|
||||
# 放在 image_analysis 特定逻辑之前,确保任何分支 return 都不会跳过本段。
|
||||
# 仅在当前值为 NULL 或过小 (<100) 时更新,不覆盖已有合理配置。
|
||||
# embedding / tts / voice_clone 不走 chat 接口,无需设置。
|
||||
default_max_tokens = {
|
||||
"intent_parsing": 500,
|
||||
"copy_fusion": 2500,
|
||||
"storyboard": 4000,
|
||||
"copy_review": 1000,
|
||||
"asset_classify": 500,
|
||||
"image_generation": 500,
|
||||
"video_generation": 500,
|
||||
}
|
||||
for cap_key, mt in default_max_tokens.items():
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"UPDATE ai_capability_configs "
|
||||
"SET max_tokens = :mt, updated_at = now() "
|
||||
"WHERE capability_key = :key "
|
||||
"AND (max_tokens IS NULL OR max_tokens < 100)"
|
||||
),
|
||||
{"mt": mt, "key": cap_key},
|
||||
)
|
||||
|
||||
# ── a. 确保 qwen-vl-plus 模型存在 ────────────────────────────────────────
|
||||
conn.execute(sa.text("""
|
||||
INSERT INTO ai_models (id, name, provider, model_key, api_key, api_base,
|
||||
description, status, is_default, usage_today,
|
||||
created_at, updated_at)
|
||||
SELECT gen_random_uuid()::text,
|
||||
'通义千问VL Plus',
|
||||
'dashscope',
|
||||
'qwen-vl-plus',
|
||||
COALESCE(
|
||||
(SELECT api_key FROM ai_models
|
||||
WHERE provider = 'dashscope' AND deleted_at IS NULL
|
||||
AND api_key IS NOT NULL AND api_key != ''
|
||||
LIMIT 1),
|
||||
''
|
||||
),
|
||||
'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
'阿里云视觉理解模型(图片识别/分析)',
|
||||
'active', false, 0, now(), now()
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM ai_models
|
||||
WHERE model_key = 'qwen-vl-plus' AND deleted_at IS NULL
|
||||
)
|
||||
"""))
|
||||
|
||||
qwen_vl_row = conn.execute(
|
||||
sa.text(
|
||||
"SELECT id FROM ai_models WHERE model_key = 'qwen-vl-plus' "
|
||||
"AND deleted_at IS NULL AND status = 'active' LIMIT 1"
|
||||
)
|
||||
).first()
|
||||
if not qwen_vl_row:
|
||||
return
|
||||
qwen_vl_id = qwen_vl_row[0]
|
||||
|
||||
qwen37_row = conn.execute(
|
||||
sa.text(
|
||||
"SELECT id FROM ai_models WHERE model_key = 'qwen3.7-plus' "
|
||||
"AND deleted_at IS NULL AND status = 'active' LIMIT 1"
|
||||
)
|
||||
).first()
|
||||
qwen37_id = qwen37_row[0] if qwen37_row else None
|
||||
|
||||
# ── b. 仅当当前 primary 不是 qwen-vl-plus 时修正绑定与 max_tokens ───────
|
||||
current = conn.execute(
|
||||
sa.text(
|
||||
"SELECT primary_model_id, lite_model_id, fallback_model_id, max_tokens "
|
||||
"FROM ai_capability_configs WHERE capability_key = 'image_analysis'"
|
||||
)
|
||||
).first()
|
||||
|
||||
if current is None:
|
||||
# capability 不存在则创建
|
||||
conn.execute(
|
||||
sa.text("""
|
||||
INSERT INTO ai_capability_configs
|
||||
(id, capability_key, capability_name, primary_model_id,
|
||||
lite_model_id, fallback_model_id, timeout_seconds,
|
||||
max_retries, max_tokens, concurrency, extra_params,
|
||||
is_enabled, created_at, updated_at)
|
||||
VALUES (gen_random_uuid()::text, 'image_analysis', '图片分析',
|
||||
:primary, :primary, :fallback, 30, 1, 1000, 2,
|
||||
'{}'::jsonb, true, now(), now())
|
||||
"""),
|
||||
{"primary": qwen_vl_id, "fallback": qwen37_id},
|
||||
)
|
||||
return
|
||||
|
||||
current_primary = current[0]
|
||||
if current_primary == qwen_vl_id:
|
||||
# 已经绑定 qwen-vl-plus:视为后台/数据迁移已处理,不覆盖任何配置
|
||||
return
|
||||
|
||||
set_parts = [
|
||||
"primary_model_id = :vl_id",
|
||||
"lite_model_id = :vl_id",
|
||||
"max_tokens = 1000",
|
||||
"updated_at = now()",
|
||||
]
|
||||
params: dict = {"vl_id": qwen_vl_id}
|
||||
if qwen37_id is not None:
|
||||
set_parts.insert(2, "fallback_model_id = :qwen37_id")
|
||||
params["qwen37_id"] = qwen37_id
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"UPDATE ai_capability_configs SET " + ", ".join(set_parts) + " WHERE capability_key = 'image_analysis'"
|
||||
),
|
||||
params,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -0,0 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""102: image_analysis max_tokens 1200 -> 1500.
|
||||
|
||||
v6 prompt 更长、字段更多,旧 max_tokens 容易截断 JSON。
|
||||
仅在 image_analysis 当前 max_tokens < 1500 时更新(幂等,不覆盖后台已调到 >=1500 的配置)。
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "102_image_analysis_max_tokens_1500"
|
||||
down_revision = "101_qwen_vl_plus_and_max_tokens"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
caps_table = conn.execute(sa.text("SELECT to_regclass('public.ai_capability_configs')")).scalar()
|
||||
if not caps_table:
|
||||
return
|
||||
|
||||
conn.execute(
|
||||
sa.text(
|
||||
"UPDATE ai_capability_configs "
|
||||
"SET max_tokens = 1500, updated_at = now() "
|
||||
"WHERE capability_key = 'image_analysis' "
|
||||
"AND (max_tokens IS NULL OR max_tokens < 1500)"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
pass
|
||||
@@ -1050,10 +1050,13 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 360px;
|
||||
padding: 28px 16px;
|
||||
gap: 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.vv-copy-loading .vv-spinner {
|
||||
width: 28px;
|
||||
@@ -1082,18 +1085,38 @@
|
||||
|
||||
/* ── Storyboard (linear doc style) ── */
|
||||
.vv-storyboard {
|
||||
padding: 6px 2px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 360px;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
margin-top: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vv-sb-doc {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
color: #1f2937;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
overflow-y: auto;
|
||||
padding-right: 4px;
|
||||
margin-right: -4px;
|
||||
}
|
||||
.vv-sb-doc::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.vv-sb-doc::-webkit-scrollbar-thumb {
|
||||
background: #d8c4ff;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.vv-sb-doc::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.vv-sb-h {
|
||||
margin: 6px 0 2px;
|
||||
@@ -1383,12 +1406,14 @@
|
||||
/* 口播稿 —— 复用 vv-sb-field 样式,无额外需求 */
|
||||
|
||||
.vv-sb-actions {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
background: #fff;
|
||||
}
|
||||
.vv-sb-actions .vv-btn-ghost {
|
||||
padding: 6px 14px;
|
||||
|
||||
@@ -264,8 +264,8 @@ def _recover_stale_jobs() -> int:
|
||||
"SET status='failed', error_msg='任务执行超时,请重试', updated_at=:now "
|
||||
"WHERE status='running' "
|
||||
" AND started_at IS NOT NULL AND started_at < :cutoff_start "
|
||||
" AND (heartbeat_at IS NULL OR heartbeat_at < :cutoff_beat) "
|
||||
" AND (heartbeat_at IS NOT NULL OR updated_at < :cutoff_beat)"
|
||||
" AND updated_at < :cutoff_beat "
|
||||
" AND (heartbeat_at IS NULL OR heartbeat_at < :cutoff_beat)"
|
||||
)
|
||||
result = ssn.execute(sql, {"now": now, "cutoff_start": cutoff_start, "cutoff_beat": cutoff_beat})
|
||||
ssn.commit()
|
||||
|
||||
@@ -170,7 +170,7 @@ def _is_v4_schema(fj: dict) -> bool:
|
||||
"""判断是v4嵌套schema还是旧扁平schema"""
|
||||
return (
|
||||
isinstance(fj.get("products"), list)
|
||||
or fj.get("type") in ("product", "store", "person", "other")
|
||||
or fj.get("type") in ("product", "store", "person", "scene", "other")
|
||||
or isinstance(fj.get("people"), dict)
|
||||
)
|
||||
|
||||
@@ -418,8 +418,31 @@ def _assemble_v4(idx: int, fj: dict, ocr_texts: list[str]) -> dict[str, Any]:
|
||||
|
||||
has_person = fj.get("has_person", False)
|
||||
|
||||
# 人物类
|
||||
if vtype == "person" or has_person:
|
||||
# 兼容老schema:无type字段或type不在已知枚举时,按has_person/products兜底
|
||||
_KNOWN_V4_TYPES = ("product", "store", "person", "scene", "other")
|
||||
if vtype not in _KNOWN_V4_TYPES:
|
||||
if has_person:
|
||||
vtype = "person"
|
||||
elif products:
|
||||
vtype = "product"
|
||||
else:
|
||||
vtype = "other"
|
||||
|
||||
# 人物信息提取辅助(scene/store分支有人物时追加到key_features)
|
||||
def _extract_person_features(source: dict) -> list[str]:
|
||||
parts = []
|
||||
for pk in ("outfit_style", "upper_wear", "lower_wear", "dress_wear", "outerwear", "pose", "expression"):
|
||||
pv = source.get(pk)
|
||||
if pv and str(pv) not in ("null", None, "无法判断") and len(str(pv)) <= 40:
|
||||
parts.append(f"人物:{pv}")
|
||||
for pk2 in ("shoes", "bag", "hairstyle"):
|
||||
pv2 = source.get(pk2)
|
||||
if pv2 and str(pv2) not in ("null", None) and len(str(pv2)) <= 40:
|
||||
parts.append(f"人物:{pv2}")
|
||||
return parts
|
||||
|
||||
# ── 人物类 ──
|
||||
if vtype == "person":
|
||||
# 取第一个人物信息(v5 schema人物信息在顶层)
|
||||
person_info = fj
|
||||
# 兼容people嵌套
|
||||
@@ -479,6 +502,12 @@ def _assemble_v4(idx: int, fj: dict, ocr_texts: list[str]) -> dict[str, Any]:
|
||||
v = person_info.get(k)
|
||||
if v and v not in ("null", None):
|
||||
kf.append(str(v))
|
||||
env_obj = person_info.get("environment_objects") or []
|
||||
if not isinstance(env_obj, list):
|
||||
env_obj = [env_obj]
|
||||
for o in env_obj:
|
||||
if o and str(o) not in kf and len(str(o)) <= 40:
|
||||
kf.append(str(o))
|
||||
if text_on_package:
|
||||
kf.append(f"文字:{'/'.join(text_on_package[:3])}")
|
||||
kf = kf[:8] or ["无法判断"]
|
||||
@@ -539,9 +568,15 @@ def _assemble_v4(idx: int, fj: dict, ocr_texts: list[str]) -> dict[str, Any]:
|
||||
for s in sell[:2]:
|
||||
if s and len(str(s)) <= 30 and str(s) not in kf:
|
||||
kf.append(f"卖点:{s}")
|
||||
env_obj = fj.get("environment_objects") or []
|
||||
if not isinstance(env_obj, list):
|
||||
env_obj = [env_obj]
|
||||
for o in env_obj:
|
||||
if o and str(o) not in kf and len(str(o)) <= 40:
|
||||
kf.append(str(o))
|
||||
if text_on_package:
|
||||
kf.append(f"文字: {'/'.join(text_on_package[:3])}")
|
||||
kf = kf[:6] or ["无法判断"]
|
||||
kf = kf[:8] or ["无法判断"]
|
||||
portrait_prompt = _build_product_prompt_from_v4(main, fj)
|
||||
if brand != "无法判断" and brand not in name:
|
||||
summary = f"{brand} {name}"
|
||||
@@ -563,57 +598,188 @@ def _assemble_v4(idx: int, fj: dict, ocr_texts: list[str]) -> dict[str, Any]:
|
||||
"_source": "v2_fast_json_v4",
|
||||
}
|
||||
|
||||
# 门店类或其他
|
||||
# 门店类
|
||||
if vtype == "store":
|
||||
store_type = fj.get("store_type") or "店铺"
|
||||
name = store_type
|
||||
brand = fj.get("brand_signage") or "无法判断"
|
||||
category = "门店场景"
|
||||
visual = fj.get("visual_elements") or []
|
||||
if isinstance(visual, str):
|
||||
visual = [visual]
|
||||
atmosphere = fj.get("atmosphere") or mood
|
||||
# appearance: store_layout + furnishings + 陈设色调
|
||||
appearance_parts = []
|
||||
if fj.get("store_layout"):
|
||||
appearance_parts.append(str(fj["store_layout"]))
|
||||
if visual:
|
||||
appearance_parts.append("、".join(str(v) for v in visual[:3]))
|
||||
furnishings = fj.get("furnishings") or []
|
||||
if isinstance(furnishings, list) and furnishings:
|
||||
appearance_parts.append("陈设:" + "、".join(str(f) for f in furnishings[:4] if f))
|
||||
if fj.get("cleanliness"):
|
||||
appearance_parts.append(str(fj["cleanliness"]))
|
||||
appearance = ";".join(appearance_parts) if appearance_parts else "门店环境"
|
||||
# key_features: 招牌细节+海报+外部物品+视觉元素+陈列商品+环境物件
|
||||
kf = []
|
||||
if isinstance(visual, list):
|
||||
kf.extend(str(v) for v in visual if v and len(str(v)) <= 30)
|
||||
for arr_key in (
|
||||
"signage_details",
|
||||
"signage_posters",
|
||||
"exterior_items",
|
||||
"visual_elements",
|
||||
"products_on_display",
|
||||
"environment_objects",
|
||||
):
|
||||
arr = fj.get(arr_key) or []
|
||||
if not isinstance(arr, list):
|
||||
arr = [arr]
|
||||
for item in arr:
|
||||
if item and str(item) not in kf and len(str(item)) <= 40:
|
||||
kf.append(str(item))
|
||||
prods_vis = fj.get("product_categories_visible") or []
|
||||
if isinstance(prods_vis, list):
|
||||
kf.extend(str(c) for c in prods_vis[:3] if c)
|
||||
for c in prods_vis[:3]:
|
||||
if c and str(c) not in kf:
|
||||
kf.append(str(c))
|
||||
promo = fj.get("promotion_elements") or []
|
||||
if isinstance(promo, list) and promo:
|
||||
kf.append("促销活动:" + "、".join(str(p) for p in promo[:2]))
|
||||
if has_person:
|
||||
for pf in _extract_person_features(fj):
|
||||
if pf not in kf:
|
||||
kf.append(pf)
|
||||
if text_on_package:
|
||||
kf.append(f"文字: {'/'.join(text_on_package[:3])}")
|
||||
kf = kf[:6] or ["门店场景"]
|
||||
portrait_prompt = f"{brand if brand!='无法判断' else ''}{store_type},{atmosphere},{scene}场景,{('、'.join(color_names[:3])+'配色,') if color_names else ''}产品陈列丰富,门店实拍"
|
||||
portrait_prompt = portrait_prompt.strip(",")
|
||||
summary = f"{store_type}场景"
|
||||
kf.append("文字:" + "/".join(text_on_package[:3]))
|
||||
kf = kf[:8] or ["门店场景"]
|
||||
atmosphere = fj.get("atmosphere") or fj.get("mood") or mood
|
||||
# portrait_prompt: 品牌+store_type+主色+atmosphere+scene+核心视觉元素
|
||||
pieces = []
|
||||
if brand != "无法判断":
|
||||
pieces.append(brand)
|
||||
pieces.append(store_type)
|
||||
if color_names:
|
||||
pieces.append("、".join(color_names[:3]) + "配色")
|
||||
if atmosphere:
|
||||
pieces.append(atmosphere)
|
||||
if scene and scene != "通用":
|
||||
pieces.append(scene)
|
||||
visual = fj.get("visual_elements") or []
|
||||
if isinstance(visual, list) and visual:
|
||||
pieces.append("、".join(str(v) for v in visual[:3] if v))
|
||||
pieces.append("门店实拍")
|
||||
portrait_prompt = ",".join(p for p in pieces if p)
|
||||
if len(portrait_prompt) > 200:
|
||||
portrait_prompt = portrait_prompt[:200].rstrip(",")
|
||||
summary = f"{brand} {store_type}" if brand != "无法判断" else f"{store_type}场景"
|
||||
return {
|
||||
"name": name[:30],
|
||||
"brand": str(brand)[:30],
|
||||
"category": category,
|
||||
"appearance": appearance[:200],
|
||||
"appearance": appearance[:300],
|
||||
"packaging": "门店场景无包装",
|
||||
"text_on_package": text_on_package,
|
||||
"key_features": kf,
|
||||
"scene": scene,
|
||||
"mood": atmosphere or mood,
|
||||
"mood": atmosphere,
|
||||
"portrait_prompt": portrait_prompt[:200],
|
||||
"summary": summary[:40],
|
||||
"has_person": False,
|
||||
"_source": "v2_fast_json_v4",
|
||||
"has_person": bool(fj.get("has_person", False)),
|
||||
"_source": "v2_fast_json_v6_store",
|
||||
}
|
||||
|
||||
# other 兜底
|
||||
desc = fj.get("description") or "未识别"
|
||||
# 场景类(纯场景图,无产品/人物)
|
||||
if vtype == "scene":
|
||||
main_subject = fj.get("main_subject") or ""
|
||||
scene_type = fj.get("scene_type") or "场景图"
|
||||
name = main_subject or scene_type
|
||||
brand = "无法判断"
|
||||
category = scene_type or "场景图"
|
||||
# appearance: 空间布局 + 关键元素
|
||||
app_parts = []
|
||||
if fj.get("spatial_layout"):
|
||||
app_parts.append(str(fj["spatial_layout"]))
|
||||
key_elements = fj.get("key_elements") or []
|
||||
if not isinstance(key_elements, list):
|
||||
key_elements = [key_elements]
|
||||
if key_elements:
|
||||
app_parts.append("关键元素:" + "、".join(str(k) for k in key_elements[:4] if k))
|
||||
if fj.get("lighting"):
|
||||
app_parts.append("光线:" + str(fj["lighting"]))
|
||||
appearance = ";".join(app_parts) if app_parts else "场景环境"
|
||||
# key_features: key_elements + environment_objects(去重,最多8)
|
||||
kf = []
|
||||
for k in key_elements:
|
||||
if k and str(k) not in kf and len(str(k)) <= 40:
|
||||
kf.append(str(k))
|
||||
env_obj = fj.get("environment_objects") or []
|
||||
if not isinstance(env_obj, list):
|
||||
env_obj = [env_obj]
|
||||
for o in env_obj:
|
||||
if o and str(o) not in kf and len(str(o)) <= 40:
|
||||
kf.append(str(o))
|
||||
if has_person:
|
||||
for pf in _extract_person_features(fj):
|
||||
if pf not in kf:
|
||||
kf.append(pf)
|
||||
if text_on_package:
|
||||
kf.append("文字:" + "/".join(text_on_package[:3]))
|
||||
kf = kf[:8] or ["场景元素"]
|
||||
scene_name = fj.get("scene") or scene_type
|
||||
scene_mood = fj.get("mood") or mood
|
||||
packaging = "场景无包装"
|
||||
# portrait_prompt: main_subject + key_elements + lighting + atmosphere + composition + style
|
||||
pieces = []
|
||||
if main_subject:
|
||||
pieces.append(main_subject)
|
||||
if key_elements:
|
||||
pieces.append("、".join(str(k) for k in key_elements[:3] if k))
|
||||
if fj.get("lighting"):
|
||||
pieces.append(str(fj["lighting"]) + "光线")
|
||||
if scene_mood:
|
||||
pieces.append(scene_mood + "氛围")
|
||||
if fj.get("composition"):
|
||||
pieces.append(str(fj["composition"]))
|
||||
if fj.get("style"):
|
||||
pieces.append(str(fj["style"]))
|
||||
pieces.append("场景实拍,摄影级画质")
|
||||
portrait_prompt = ",".join(p for p in pieces if p)
|
||||
if len(portrait_prompt) > 200:
|
||||
portrait_prompt = portrait_prompt[:200].rstrip(",")
|
||||
summary = main_subject or scene_type
|
||||
return {
|
||||
"name": str(name)[:30],
|
||||
"brand": brand,
|
||||
"category": str(category)[:20],
|
||||
"appearance": appearance[:300],
|
||||
"packaging": packaging,
|
||||
"text_on_package": text_on_package,
|
||||
"key_features": kf,
|
||||
"scene": scene_name,
|
||||
"mood": scene_mood,
|
||||
"portrait_prompt": portrait_prompt[:200],
|
||||
"summary": str(summary)[:40],
|
||||
"has_person": bool(fj.get("has_person", False)),
|
||||
"_source": "v2_fast_json_v6_scene",
|
||||
}
|
||||
|
||||
# other 兜底:当 description 为空时,用 environment_objects/scene 拼基本描述
|
||||
desc = fj.get("description") or ""
|
||||
if not desc.strip():
|
||||
env_obj = fj.get("environment_objects") or []
|
||||
if not isinstance(env_obj, list):
|
||||
env_obj = [env_obj]
|
||||
env_names = [str(e) for e in env_obj if e and len(str(e)) <= 25][:5]
|
||||
scene_text = fj.get("scene") or ""
|
||||
if env_names:
|
||||
desc = f"{scene_text}场景中" + "、".join(env_names) if scene_text else "、".join(env_names)
|
||||
elif scene_text and scene_text != "通用":
|
||||
desc = f"{scene_text}场景"
|
||||
else:
|
||||
desc = "未识别"
|
||||
kf_other = []
|
||||
if desc != "未识别":
|
||||
kf_other.append(desc[:30])
|
||||
env_obj = fj.get("environment_objects") or []
|
||||
if not isinstance(env_obj, list):
|
||||
env_obj = [env_obj]
|
||||
for o in env_obj:
|
||||
if o and str(o) not in kf_other and len(str(o)) <= 40:
|
||||
kf_other.append(str(o))
|
||||
kf_other = kf_other[:8] or ["无法判断"]
|
||||
return {
|
||||
"name": desc[:30],
|
||||
"brand": "无法判断",
|
||||
@@ -621,7 +787,7 @@ def _assemble_v4(idx: int, fj: dict, ocr_texts: list[str]) -> dict[str, Any]:
|
||||
"appearance": desc[:200],
|
||||
"packaging": "无法判断",
|
||||
"text_on_package": text_on_package,
|
||||
"key_features": [desc[:30]] if desc != "未识别" else ["无法判断"],
|
||||
"key_features": kf_other,
|
||||
"scene": scene,
|
||||
"mood": mood,
|
||||
"portrait_prompt": f"{scene},{mood}氛围,{desc}"[:200],
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""V2 兜底路径:image_analysis lite/fallback(默认 qwen3.7-plus / DashScope)单图调用。
|
||||
"""V2 兜底路径:image_analysis(默认 qwen-vl-plus 视觉模型,fallback qwen3.7-plus / DashScope)单图调用。
|
||||
|
||||
fast_json 超时/返回非 JSON/识别为空时,本路径单次调用兜底。
|
||||
设计要点:
|
||||
- 通过 ai_router.get_vision_client() 获取 DoubaoClient 实例,不再自己拼 httpx 请求
|
||||
- enable_thinking=False + response_format=json_object
|
||||
- system prompt 优先读后台 viral_video_prompt_templates 配置,DB不可用时fallback到硬编码JSON schema
|
||||
- timeout=25s
|
||||
- max_tokens 不传,使用 client 中 capability 的 DB 配置(避免硬编码截断 JSON)
|
||||
- timeout=30s
|
||||
- 返回 dict 统一走 assembler.assemble_result 组装,与 fast 路径输出格式完全一致
|
||||
"""
|
||||
|
||||
@@ -22,7 +23,6 @@ from . import _prompt, assembler
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_TIMEOUT = 30
|
||||
_DEFAULT_MAX_TOKENS = 800
|
||||
|
||||
|
||||
def call_pro_vlm(
|
||||
@@ -30,7 +30,9 @@ def call_pro_vlm(
|
||||
idx: int,
|
||||
*,
|
||||
timeout: int = _DEFAULT_TIMEOUT,
|
||||
max_tokens: int | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""max_tokens 默认 None:不显式传参,使用 client 内 capability 的 DB 配置。"""
|
||||
t0 = time.time()
|
||||
|
||||
try:
|
||||
@@ -58,15 +60,17 @@ def call_pro_vlm(
|
||||
]
|
||||
|
||||
try:
|
||||
raw = client.vision_completion(
|
||||
messages=messages,
|
||||
images=None, # 图片已在 messages 中
|
||||
temperature=0.3,
|
||||
max_tokens=_DEFAULT_MAX_TOKENS,
|
||||
timeout=timeout,
|
||||
enable_thinking=False,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
call_kwargs: dict[str, Any] = {
|
||||
"messages": messages,
|
||||
"images": None, # 图片已在 messages 中
|
||||
"temperature": 0.3,
|
||||
"timeout": timeout,
|
||||
"enable_thinking": False,
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
if max_tokens is not None:
|
||||
call_kwargs["max_tokens"] = max_tokens
|
||||
raw = client.vision_completion(**call_kwargs)
|
||||
elapsed = time.time() - t0
|
||||
if not raw:
|
||||
logger.warning("[vision.v2] pro 返回空 elapsed=%.1fs", elapsed)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""V2 快速路径:image_analysis capability(默认 qwen3.8-flash / DashScope)强约束 JSON-only 调用。
|
||||
"""V2 快速路径:image_analysis capability(默认 qwen-vl-plus 视觉模型 / DashScope)强约束 JSON-only 调用。
|
||||
|
||||
目标:替代"人体属性/商品检测/图像标签"三个火山不存在的专用云端 API。
|
||||
设计要点:
|
||||
@@ -7,8 +7,9 @@
|
||||
- enable_thinking=False 关闭推理链(reasoning 是延迟主因)
|
||||
- response_format=json_object 强约束JSON输出
|
||||
- system prompt 优先读后台 viral_video_prompt_templates 配置,DB不可用时fallback到硬编码JSON schema
|
||||
- max_tokens=350、temperature=0.1(稳定输出 JSON)
|
||||
- timeout=12s(失败由外层走 pro 兜底)
|
||||
- max_tokens 不传,使用 client 中 capability 的 DB 配置(避免硬编码截断 JSON)
|
||||
- temperature=0.1(稳定输出 JSON)
|
||||
- timeout=15s(失败由外层走 pro 兜底)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -23,7 +24,6 @@ from . import _prompt
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_DEFAULT_TIMEOUT = 15
|
||||
_DEFAULT_MAX_TOKENS = 350
|
||||
|
||||
|
||||
def _strip_code_fence(s: str) -> str:
|
||||
@@ -42,9 +42,13 @@ def call_fast_json(
|
||||
img_url: str,
|
||||
*,
|
||||
timeout: int = _DEFAULT_TIMEOUT,
|
||||
max_tokens: int = _DEFAULT_MAX_TOKENS,
|
||||
max_tokens: int | None = None,
|
||||
) -> dict[str, Any] | None:
|
||||
"""调用 vision client 返回结构化 dict;失败/非 JSON 返回 None。"""
|
||||
"""调用 vision client 返回结构化 dict;失败/非 JSON 返回 None。
|
||||
|
||||
max_tokens 默认 None:不显式传参,使用 client 内 capability 的 DB 配置;
|
||||
显式传入时作为覆盖。
|
||||
"""
|
||||
t0 = time.time()
|
||||
|
||||
try:
|
||||
@@ -72,15 +76,17 @@ def call_fast_json(
|
||||
]
|
||||
|
||||
try:
|
||||
raw = client.vision_completion(
|
||||
messages=messages,
|
||||
images=None, # 图片已在 messages 中
|
||||
temperature=0.1,
|
||||
max_tokens=max_tokens,
|
||||
timeout=timeout,
|
||||
enable_thinking=False,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
call_kwargs: dict[str, Any] = {
|
||||
"messages": messages,
|
||||
"images": None, # 图片已在 messages 中
|
||||
"temperature": 0.1,
|
||||
"timeout": timeout,
|
||||
"enable_thinking": False,
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
if max_tokens is not None:
|
||||
call_kwargs["max_tokens"] = max_tokens
|
||||
raw = client.vision_completion(**call_kwargs)
|
||||
elapsed = time.time() - t0
|
||||
if not raw:
|
||||
logger.warning("[vision.v2] fast_json 返回空 elapsed=%.1fs", elapsed)
|
||||
|
||||
@@ -369,7 +369,7 @@ class CosyVoiceService:
|
||||
self._api_key = api_key or _router_key or settings.cosyvoice_api_key
|
||||
self._base_url = base_url or _router_url or settings.cosyvoice_base_url
|
||||
self._model = model or _router_model or settings.cosyvoice_model
|
||||
self._clone_model = clone_model or getattr(settings, "cosyvoice_clone_model", "voice-enrollment")
|
||||
self._clone_model = clone_model or getattr(settings, "cosyvoice_clone_model", "")
|
||||
self._audio_url_signer = audio_url_signer
|
||||
|
||||
# base_url 规范化:去掉末尾的路径残留(兼容旧版配置)
|
||||
|
||||
@@ -225,8 +225,11 @@ class ViralVideoJob:
|
||||
self.error_msg = ""
|
||||
self.completed_at = None
|
||||
self.heartbeat_at = None
|
||||
_now = datetime.now(timezone.utc)
|
||||
self.started_at = _now
|
||||
self.heartbeat_at = _now
|
||||
self.status = ViralVideoStatus.RUNNING
|
||||
self.updated_at = datetime.now(timezone.utc)
|
||||
self.updated_at = _now
|
||||
|
||||
def resume_from_copy_generated(self, edited_copy: str | None = None) -> None:
|
||||
"""阶段2->阶段3:用户确认/编辑口播文案,开始跑 TTS+单次Seedance渲染。"""
|
||||
@@ -235,14 +238,20 @@ class ViralVideoJob:
|
||||
if edited_copy and isinstance(self.copy_result, dict):
|
||||
self.copy_result = {**self.copy_result, "voiceover_script": edited_copy}
|
||||
self.generated_copy_text = edited_copy
|
||||
_now = datetime.now(timezone.utc)
|
||||
self.started_at = _now
|
||||
self.heartbeat_at = _now
|
||||
self.status = ViralVideoStatus.RUNNING
|
||||
self.updated_at = datetime.now(timezone.utc)
|
||||
self.updated_at = _now
|
||||
|
||||
def resume_from_confirm(self) -> None:
|
||||
if self.status != ViralVideoStatus.WAIT_USER_CONFIRM:
|
||||
raise ValueError(f"Cannot resume from {self.status}")
|
||||
_now = datetime.now(timezone.utc)
|
||||
self.started_at = _now
|
||||
self.heartbeat_at = _now
|
||||
self.status = ViralVideoStatus.RUNNING
|
||||
self.updated_at = datetime.now(timezone.utc)
|
||||
self.updated_at = _now
|
||||
|
||||
def mark_completed(self, video_url: str) -> None:
|
||||
self.status = ViralVideoStatus.COMPLETED
|
||||
|
||||
@@ -183,15 +183,20 @@ class DoubaoClient:
|
||||
provider: str = "volcengine",
|
||||
) -> None:
|
||||
settings = get_shared_settings()
|
||||
self.api_key: str = api_key or settings.doubao_api_key
|
||||
self.model: str = model or settings.doubao_model
|
||||
self.base_url: str = (base_url or settings.doubao_base_url).rstrip("/")
|
||||
self.provider: str = provider
|
||||
if provider == "dashscope":
|
||||
self.api_key: str = api_key or getattr(settings, "dashscope_api_key", "")
|
||||
self.model: str = model or getattr(settings, "dashscope_model", "")
|
||||
self.base_url: str = (base_url or getattr(settings, "dashscope_base_url", "")).rstrip("/")
|
||||
else: # volcengine (default)
|
||||
self.api_key = api_key or settings.doubao_api_key
|
||||
self.model = model or settings.doubao_model
|
||||
self.base_url = (base_url or settings.doubao_base_url).rstrip("/")
|
||||
self.timeout: int = timeout or settings.doubao_timeout
|
||||
self.max_retries: int = max_retries or settings.doubao_max_retries
|
||||
self.max_tokens: int | None = max_tokens
|
||||
self.temperature: float | None = temperature
|
||||
self.extra_params: dict = extra_params or {}
|
||||
self.provider: str = provider
|
||||
self.vision_model: str = settings.doubao_vision_model
|
||||
self.vision_lite_model: str = settings.doubao_vision_lite_model
|
||||
self.fast_model: str = settings.doubao_fast_model
|
||||
@@ -255,7 +260,7 @@ class DoubaoClient:
|
||||
self,
|
||||
messages: list[dict[str, str]],
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1024,
|
||||
max_tokens: int | None = None,
|
||||
model: str | None = None,
|
||||
timeout: int | None = None,
|
||||
**kwargs,
|
||||
@@ -265,7 +270,7 @@ class DoubaoClient:
|
||||
Args:
|
||||
messages: 对话消息列表,[{"role": "user"/"system"/"assistant", "content": "..."}]
|
||||
temperature: 采样温度,0-2,默认0.7
|
||||
max_tokens: 最大生成token数,默认1024
|
||||
max_tokens: 最大生成token数,默认 None(使用实例 self.max_tokens DB 配置,兜底 1024)
|
||||
|
||||
Returns:
|
||||
模型返回的文本内容,失败返回 None
|
||||
@@ -278,11 +283,12 @@ class DoubaoClient:
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
effective_max_tokens = max_tokens if max_tokens is not None else (self.max_tokens or 1024)
|
||||
payload: dict[str, Any] = {
|
||||
"model": model or self.model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"max_tokens": effective_max_tokens,
|
||||
}
|
||||
# 合并实例级额外参数和调用方传入的额外参数
|
||||
if self.extra_params:
|
||||
@@ -303,6 +309,22 @@ class DoubaoClient:
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
finish_reason = (data.get("choices") or [{}])[0].get("finish_reason", "")
|
||||
if finish_reason == "length" and attempt < self.max_retries:
|
||||
# 输出被 max_tokens 截断:1.5x 扩容后重试(计入 max_retries,不额外增加)
|
||||
old_max = int(payload["max_tokens"])
|
||||
new_max = int(old_max * 1.5)
|
||||
payload["max_tokens"] = new_max
|
||||
wait = 0.5 * (2**attempt)
|
||||
logger.warning(
|
||||
"输出被max_tokens截断(%d),扩容到%d后重试 (第%d/%d次)",
|
||||
old_max,
|
||||
new_max,
|
||||
attempt + 1,
|
||||
self.max_retries + 1,
|
||||
)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
_elapsed = time.time() - _t0
|
||||
logger.info(
|
||||
@@ -336,7 +358,7 @@ class DoubaoClient:
|
||||
self,
|
||||
messages: list[dict],
|
||||
images: list[str] | None = None,
|
||||
max_tokens: int = 2048,
|
||||
max_tokens: int | None = None,
|
||||
temperature: float = 0.3,
|
||||
timeout: int | None = None,
|
||||
model: str | None = None,
|
||||
@@ -345,12 +367,12 @@ class DoubaoClient:
|
||||
"""调用豆包视觉理解 API(OpenAI 兼容多模态格式).
|
||||
|
||||
将 images 附加到最后一条 user message 的 content 中,
|
||||
使用 vision_model(默认 doubao-1-5-vision-pro-250328)。
|
||||
使用构造函数传入的 self.model(DB capability 绑定的视觉模型,默认 qwen-vl-plus)。
|
||||
|
||||
Args:
|
||||
messages: 对话消息列表。最后一条 user message 会被注入图片内容。
|
||||
images: 图片列表,支持 base64 data URI 或 HTTP(S) URL。
|
||||
max_tokens: 最大生成 token 数,默认 2048。
|
||||
max_tokens: 最大生成 token 数,默认 None(使用实例 self.max_tokens DB 配置,兜底 2048)。
|
||||
temperature: 采样温度,默认 0.3(视觉任务偏低更稳定)。
|
||||
timeout: 单次请求超时秒数,不传则使用默认 self.timeout。
|
||||
|
||||
@@ -390,11 +412,12 @@ class DoubaoClient:
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
effective_max_tokens = max_tokens if max_tokens is not None else (self.max_tokens or 2048)
|
||||
payload: dict[str, Any] = {
|
||||
"model": model or self.vision_model,
|
||||
"model": model or self.model,
|
||||
"messages": vision_messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"max_tokens": effective_max_tokens,
|
||||
}
|
||||
if self.extra_params:
|
||||
payload.update(self.extra_params)
|
||||
@@ -414,6 +437,22 @@ class DoubaoClient:
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
finish_reason = (data.get("choices") or [{}])[0].get("finish_reason", "")
|
||||
if finish_reason == "length" and attempt < self.max_retries:
|
||||
# 视觉输出被 max_tokens 截断:1.5x 扩容后重试(计入 max_retries)
|
||||
old_max = int(payload["max_tokens"])
|
||||
new_max = int(old_max * 1.5)
|
||||
payload["max_tokens"] = new_max
|
||||
wait = 0.5 * (2**attempt)
|
||||
logger.warning(
|
||||
"视觉输出被max_tokens截断(%d),扩容到%d后重试 (第%d/%d次)",
|
||||
old_max,
|
||||
new_max,
|
||||
attempt + 1,
|
||||
self.max_retries + 1,
|
||||
)
|
||||
time.sleep(wait)
|
||||
continue
|
||||
content = data["choices"][0]["message"]["content"]
|
||||
_elapsed = time.time() - _t0
|
||||
logger.info(
|
||||
|
||||
@@ -429,8 +429,8 @@ class AIRouter:
|
||||
api_key = getattr(settings, "cosyvoice_api_key", "")
|
||||
if not api_key:
|
||||
return None
|
||||
base_url = getattr(settings, "cosyvoice_base_url", "https://dashscope.aliyuncs.com/api/v1")
|
||||
model = getattr(settings, "cosyvoice_model", "cosyvoice-v3-flash")
|
||||
base_url = getattr(settings, "cosyvoice_base_url", "")
|
||||
model = getattr(settings, "cosyvoice_model", "")
|
||||
|
||||
return TTSClient(provider="dashscope", api_key=api_key, base_url=base_url, model=model)
|
||||
|
||||
@@ -439,8 +439,8 @@ class AIRouter:
|
||||
api_key = getattr(settings, "doubao_api_key", "")
|
||||
if not api_key:
|
||||
return None
|
||||
base_url = getattr(settings, "doubao_base_url", "https://ark.cn-beijing.volces.com/api/v3")
|
||||
model = getattr(settings, "doubao_image_model", "doubao-seedream-5-0-flash-260915")
|
||||
base_url = getattr(settings, "doubao_base_url", "")
|
||||
model = getattr(settings, "doubao_image_model", "")
|
||||
|
||||
return ImageGenClient(
|
||||
provider="volcengine",
|
||||
@@ -455,8 +455,8 @@ class AIRouter:
|
||||
api_key = getattr(settings, "doubao_api_key", "")
|
||||
if not api_key:
|
||||
return None
|
||||
base_url = getattr(settings, "doubao_base_url", "https://ark.cn-beijing.volces.com/api/v3")
|
||||
model = getattr(settings, "doubao_video_model", "doubao-seedance-2-5-260628")
|
||||
base_url = getattr(settings, "doubao_base_url", "")
|
||||
model = getattr(settings, "doubao_video_model", "")
|
||||
|
||||
return VideoGenClient(
|
||||
provider="volcengine",
|
||||
|
||||
Reference in New Issue
Block a user