Skip to content

Commit d87ac58

Browse files
authored
Merge pull request #65 from fulai-tech/qlj/dev/0824-somni-audio-service
量产接口: 完善问卷答案与音频目录查询
2 parents 26bd4da + 41011f7 commit d87ac58

18 files changed

Lines changed: 1138 additions & 123 deletions

‎app/core/config.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class Settings(BaseSettings):
5454
somni_mongo_answers_collection: str = "somni_quiz_answers"
5555

5656
sim_threshold: float = 0.7 # 内容形态向量模糊命中阈值(规范 §五-2)
57+
# GetAudio query_text 与根标签向量相似度下限
58+
get_audio_root_tag_sim_threshold: float = 0.85
5759
# 多路文本检索厌恶硬剔除阈值;≥ 该值 penalty=1.0 丢弃候选
5860
strong_dislike_sim_threshold: float = 0.85
5961
search_sleep_stage_filter_enabled: bool = True # 检索步骤 1 是否按睡眠阶段过滤
@@ -93,6 +95,7 @@ class Settings(BaseSettings):
9395
redis_max_connections: int = 512
9496
search_cache_max_size: int = 2048
9597
search_cache_ttl_sec: int = 604800 # 7 天
98+
somni_audio_catalog_cache_ttl_sec: float = 60.0
9699
# CUD 后延时重建睡眠阶段候选缓存,窗口内多次写入只重建一次
97100
sleep_stage_cache_rewarm_delay_sec: float = 5.0
98101

‎app/es/search.py‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,20 @@ def _candidate_search_body(query: dict[str, Any], *, size: int = 1000) -> dict[s
9797
class EsSearch:
9898
"""封装检索相关的 ES 查询与文档解析。"""
9999

100-
def __init__(self, client: AsyncElasticsearch, settings: Settings) -> None:
100+
def __init__(
101+
self,
102+
client: AsyncElasticsearch,
103+
settings: Settings,
104+
*,
105+
audio_index: str | None = None,
106+
tag_dictionary_index: str | None = None,
107+
) -> None:
101108
self._client = client
102109
self._settings = settings
110+
self._audio_index = audio_index or settings.es_audio_index
111+
self._tag_dictionary_index = (
112+
tag_dictionary_index or settings.es_tag_vectors_index
113+
)
103114
self._content_tag_vectors_cache: list[dict[str, Any]] | None = None
104115
self._content_tag_vectors_lock = asyncio.Lock()
105116
# 按 tag_id 缓存 name_vector,避免每请求 mget(内容准入模糊路径)
@@ -108,11 +119,11 @@ def __init__(self, client: AsyncElasticsearch, settings: Settings) -> None:
108119

109120
@property
110121
def audio_index(self) -> str:
111-
return self._settings.es_audio_index
122+
return self._audio_index
112123

113124
@property
114125
def tag_dictionary_index(self) -> str:
115-
return self._settings.es_tag_vectors_index
126+
return self._tag_dictionary_index
116127

117128
@property
118129
def tag_vectors_index(self) -> str:
@@ -321,6 +332,7 @@ async def _fetch_content_tag_vectors(self, *, size: int) -> list[dict[str, Any]]
321332
"label": label,
322333
"dimension": source.get("type", ""),
323334
"vector": vector,
335+
"parent_tag_id": str(source.get("parent_tag_id") or ""),
324336
}
325337
)
326338
return tags
@@ -380,6 +392,18 @@ def content_tag_ids(tags: AudioTags) -> list[str]:
380392
ids.extend(item.vector_id for item in dim)
381393
return ids
382394

395+
async def list_audio_catalog_docs(self, *, size: int) -> list[dict[str, Any]]:
396+
"""量产 GetAudio:音频全量(不含 embedding),供内存过滤。"""
397+
response = await self._client.search(
398+
index=self.audio_index,
399+
body={
400+
"query": {"match_all": {}},
401+
"size": max(1, size),
402+
"_source": {"excludes": ["embedding", "description_vector"]},
403+
},
404+
)
405+
return [_document_from_hit(hit) for hit in response["hits"]["hits"]]
406+
383407
async def migrate_legacy_indices(self) -> None:
384408
"""删除旧版 audio_materials / tag_vectors 索引。"""
385409
for index in LEGACY_INDICES:

‎app/main.py‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def _bootstrap_dev_entry() -> None:
6363
from app.server.bootstrap import GrpcServers, start_grpc_servers, stop_grpc_servers
6464
from app.server.handboard.audio.service import AudioService
6565
from app.server.handboard.audio.store import MaterialsStore, create_materials_store
66+
from app.server.somni.audio.catalog import AudioCatalogService as SomniAudioService
6667
from app.server.somni.quiz.service import QuizService as SomniQuizService
6768
from app.server.somni.report.service import ReportService as SomniReportService
6869
from app.services.retrieval import RetrievalService
@@ -73,6 +74,7 @@ def _bootstrap_dev_entry() -> None:
7374
class AppState:
7475
settings: Settings
7576
es_client: AsyncElasticsearch | None = None
77+
somni_es_client: AsyncElasticsearch | None = None
7678
encoder: Encoder | None = None
7779
materials_store: MaterialsStore | None = None
7880
somni_mongo_client: AsyncIOMotorClient | None = None
@@ -82,6 +84,7 @@ class AppState:
8284
audio_service: AudioService | None = None
8385
somni_quiz_service: SomniQuizService | None = None
8486
somni_report_service: SomniReportService | None = None
87+
somni_audio_service: SomniAudioService | None = None
8588
search_cache: AudioSearchCache | None = None
8689
sleep_stage_cache: SleepStageCandidateCache | None = None
8790
grpc_servers: GrpcServers | None = None
@@ -171,10 +174,27 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
171174
somni_mongo = AsyncIOMotorClient(settings.somni_mongo_uri)
172175
_app_state.somni_mongo_client = somni_mongo
173176
else:
174-
logger.warning("未配置 SOMNI_MONGO_URI,量产问卷将不可用")
177+
logger.warning("未配置 SOMNI_MONGO_URI,量产问卷与音频查询将不可用")
175178

176179
_app_state.somni_quiz_service = SomniQuizService(somni_mongo, settings)
177180
_app_state.somni_report_service = SomniReportService()
181+
somni_es_client = create_es_client(
182+
settings,
183+
node=settings.effective_somni_es_node,
184+
)
185+
_app_state.somni_es_client = somni_es_client
186+
somni_es_search = EsSearch(
187+
somni_es_client,
188+
settings,
189+
audio_index=settings.somni_es_audio_index,
190+
tag_dictionary_index=settings.somni_es_tag_vectors_index,
191+
)
192+
_app_state.somni_audio_service = SomniAudioService(
193+
somni_mongo,
194+
settings,
195+
es_search=somni_es_search,
196+
encoder=encoder,
197+
)
178198

179199
start_sync_scheduler(_app_state, settings)
180200
_app_state.grpc_servers = await start_grpc_servers(_app_state, settings)
@@ -191,6 +211,7 @@ async def lifespan(app: FastAPI) -> AsyncIterator[None]:
191211
materials_store.close()
192212
if somni_mongo is not None:
193213
somni_mongo.close()
214+
await somni_es_client.close()
194215
await es_client.close()
195216

196217

‎app/server/bootstrap.py‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,20 @@
66
from typing import TYPE_CHECKING
77

88
import grpc
9+
from grpc_reflection.v1alpha import reflection
910
from loguru import logger
1011

1112
from app.server.handboard.audio.rpc import AudioRpc as HandboardAudioRpc
1213
from app.server.handboard.quiz.rpc import QuizRpc as HandboardQuizRpc
14+
from app.server.somni.audio.rpc import AudioRpc as SomniAudioRpc
1315
from app.server.somni.quiz.rpc import QuizRpc as SomniQuizRpc
1416
from app.server.somni.report.rpc import ReportRpc as SomniReportRpc
15-
from app.uburnode_grpc.grpc_gen import uburnode_pb2_grpc, uburnode_somni_pb2_grpc
17+
from app.uburnode_grpc.grpc_gen import (
18+
uburnode_pb2,
19+
uburnode_pb2_grpc,
20+
uburnode_somni_pb2,
21+
uburnode_somni_pb2_grpc,
22+
)
1623

1724
if TYPE_CHECKING:
1825
from app.core.config import Settings
@@ -55,6 +62,7 @@ async def _start_handboard(state: AppState, settings: Settings) -> grpc.aio.Serv
5562
server,
5663
)
5764
uburnode_pb2_grpc.add_QuizServiceServicer_to_server(HandboardQuizRpc(), server)
65+
_enable_reflection(server, uburnode_pb2)
5866
bind = f"{settings.grpc_host}:{settings.grpc_port}"
5967
_bind(server, bind, "功能手板")
6068
await server.start()
@@ -71,12 +79,23 @@ async def _start_somni(state: AppState, settings: Settings) -> grpc.aio.Server:
7179
SomniReportRpc(getattr(state, "somni_report_service", None)),
7280
server,
7381
)
82+
uburnode_somni_pb2_grpc.add_AudioServiceServicer_to_server(
83+
SomniAudioRpc(getattr(state, "somni_audio_service", None)),
84+
server,
85+
)
86+
_enable_reflection(server, uburnode_somni_pb2)
7487
bind = f"{settings.grpc_host}:{settings.somni_grpc_port}"
7588
_bind(server, bind, "量产")
7689
await server.start()
7790
return server
7891

7992

93+
def _enable_reflection(server: grpc.aio.Server, proto_module) -> None:
94+
names = [reflection.SERVICE_NAME]
95+
names.extend(svc.full_name for svc in proto_module.DESCRIPTOR.services_by_name.values())
96+
reflection.enable_server_reflection(tuple(names), server)
97+
98+
8099
def _bind(server: grpc.aio.Server, bind: str, label: str) -> None:
81100
if server.add_insecure_port(bind) == 0:
82101
raise RuntimeError(f"{label} gRPC 无法绑定 {bind}")

0 commit comments

Comments
 (0)