HomePilot Deploy Bot
chore(hf): sync HomePilot to HF Space
921d377
Raw
History Blame Contribute Delete
4.64 kB
"""
ix_character_assets library — reusable per-persona / per-mood clips.
Phase 1 provides the query + register primitives. Phase 2 will add
LLM/heuristic ranking to pick the 'most fitting' clip for a given
mood × action × language combination.
"""
from __future__ import annotations
import json
import sqlite3
from typing import Any, Dict, List, Optional
from .. import store
from ..models import CharacterAsset
def _row_to_asset(row: sqlite3.Row) -> CharacterAsset:
d = store.row_to_dict(
row,
json_fields=("mood_tags", "action_tags", "outfit_tags"),
)
return CharacterAsset(**d)
def register_library_asset(
*,
persona_id: str,
asset_id: str,
kind: str,
mood_tags: Optional[List[str]] = None,
action_tags: Optional[List[str]] = None,
language: str = "",
outfit_tags: Optional[List[str]] = None,
duration_sec: float = 0.0,
intensity: float = 0.5,
) -> CharacterAsset:
"""Register a file_assets row as a character-library entry.
The ``asset_id`` is expected to already exist in ``file_assets``
— this module doesn't upload files; it only adds a tagged
pointer row in ``ix_character_assets``.
"""
store.ensure_schema()
ix_id = store.new_id("ixa")
with store._conn() as con:
con.execute(
"""
INSERT INTO ix_character_assets (
id, persona_id, asset_id, kind,
mood_tags, action_tags, language, outfit_tags,
duration_sec, intensity
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""",
(
ix_id,
persona_id,
asset_id,
kind,
store._dump_json(mood_tags or []),
store._dump_json(action_tags or []),
language,
store._dump_json(outfit_tags or []),
float(duration_sec),
float(intensity),
),
)
con.commit()
row = con.execute(
"SELECT * FROM ix_character_assets WHERE id = ?", (ix_id,),
).fetchone()
return _row_to_asset(row)
def query_library(
persona_id: str,
*,
mood: Optional[str] = None,
action: Optional[str] = None,
language: Optional[str] = None,
kind: Optional[str] = None,
limit: int = 20,
) -> List[CharacterAsset]:
"""Query the library for matching clips. Scoring is done in
Python so the SQL stays simple and correct.
Returned rows are sorted by match score (desc), then by
intensity (desc). Score awards points for each tag hit.
"""
store.ensure_schema()
with store._conn() as con:
rows = con.execute(
"SELECT * FROM ix_character_assets WHERE persona_id = ? "
+ ("AND kind = ? " if kind else "")
+ "ORDER BY intensity DESC LIMIT ?",
(persona_id, kind, max(1, min(limit, 200))) if kind
else (persona_id, max(1, min(limit, 200))),
).fetchall()
candidates: List[tuple] = []
for row in rows:
asset = _row_to_asset(row)
score = 0
if mood and mood in (asset.mood_tags or []):
score += 3
if action and action in (asset.action_tags or []):
score += 2
if language and (not asset.language or asset.language == language):
score += 1
candidates.append((score, asset))
candidates.sort(key=lambda t: (-t[0], -t[1].intensity))
return [c[1] for c in candidates]
# Bootstrap helper — lets tests create a minimal library without
# needing to pre-populate file_assets.
def seed_library_defaults(persona_id: str) -> List[str]:
"""Seed one no-op 'idle' + one 'reaction' asset for a persona.
Returns the ix_character_assets.id list. Used by tests + the
admin 'initialize library' action. Skips if the persona already
has any entries.
"""
store.ensure_schema()
with store._conn() as con:
existing = con.execute(
"SELECT COUNT(*) FROM ix_character_assets WHERE persona_id = ?",
(persona_id,),
).fetchone()[0]
if existing:
return []
ids = []
ids.append(register_library_asset(
persona_id=persona_id, asset_id=f"placeholder-idle-{persona_id}",
kind="idle_loop", mood_tags=["neutral"], duration_sec=6.0, intensity=0.3,
).id)
ids.append(register_library_asset(
persona_id=persona_id, asset_id=f"placeholder-reaction-{persona_id}",
kind="reaction", mood_tags=["flirty"], action_tags=["a_greet"],
duration_sec=3.0, intensity=0.6,
).id)
return ids