diff --git a/backend/.python-version b/backend/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/backend/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..c4b427c --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,60 @@ +# syntax=docker/dockerfile:1 +# ============================================================ +# OPC 智能园区后端(FastAPI + s2s 语音栈)Docker 镜像 +# 构建: docker build -f backend/Dockerfile -t dpm-backend . +# 运行: docker compose up -d --build (推荐,见根目录 docker-compose.yml) +# 说明: +# · 依赖使用 uv 按 uv.lock 锁定安装(Python 3.12) +# · vendor/s2s-cloud(补丁版语音栈)以 --no-deps 方式安装,避免拖入多余依赖 +# · silero VAD / nltk 数据 / YOLO 模型随镜像打包,运行期不联网下载 +# · media / knowledge / data.json 挂载为持久卷(数据与知识库可热更新) +# ============================================================ +FROM python:3.12-slim-bookworm + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + UV_LINK_MODE=copy \ + UV_PROJECT_ENVIRONMENT=/opt/venv \ + PATH="/opt/venv/bin:$PATH" + +# 系统依赖:libsndfile1(soundfile 音频解码)、libgomp1(torch)、 +# libgl1/libglib2.0-0(OpenCV/ultralytics 图像处理) +RUN apt-get update && apt-get install -y --no-install-recommends \ + libsndfile1 \ + libgomp1 \ + libgl1 \ + libglib2.0-0 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# uv 包管理器(按 lock 锁定版本安装) +COPY --from=ghcr.io/astral-sh/uv:0.7.4 /uv /uvx /usr/local/bin/ + +WORKDIR /app +COPY backend /app/backend + +WORKDIR /app/backend + +# 1) 按 uv.lock 安装全部依赖(不含 dev;不安装项目本身,保持 /app/backend 为源码目录) +RUN uv sync --locked --no-dev --no-install-project + +# 2) 安装补丁后的 s2s 语音栈(--no-deps:仅装包体,依赖已由 uv sync 覆盖) +RUN uv pip install --no-deps --no-build-isolation -e vendor/s2s-cloud + +# 3) 运行期资源目录(本地缓存随镜像打包,避免运行期联网下载) +ENV TORCH_HOME=/app/backend/.torch-cache \ + NLTK_DATA=/app/backend/nltk_data \ + YOLO_CONFIG_DIR=/app/backend/.ultralytics \ + NO_PROXY=* \ + no_proxy=* + +# 持久卷:媒体资源 / 知识库 / 运行数据 +VOLUME ["/app/backend/media", "/app/backend/knowledge", "/app/backend/data.json"] + +EXPOSE 10085 8765 + +HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=5 \ + CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://192.168.1.9:10085/api/health', timeout=4).status==200 else 1)" || exit 1 + +CMD ["python", "docker_entry.py"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..1092b36 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,115 @@ +# OPC 智能园区后端(FastAPI + MQTT) + +独立后端服务:**所有数据通过 REST API 提供**,**页面/媒体控制通过 MQTT 下发**,媒体资源由后端统一存储与返回。 + +## 架构 + +``` +┌──────────────┐ REST(全部数据/媒体/设置) ┌────────────────┐ +│ Tauri 大屏 │ ◄────────────────────────── │ FastAPI :10085 │ +│ (React 前端) │ └───────┬────────┘ +│ │ MQTT 订阅 opc/display/command │ MQTT 发布 +│ 账号: dpm │ ◄────────────────────────────────────│ 账号: dpmserver +└──────┬───────┘ MQTT Broker (EMQX) │ + │ 192.168.1.3:1883 │ + └───────────────────────────────────────────────┘ +``` + +- **数据**:`GET /api/dashboard/snapshot`(每 2.2s 刷新,模拟引擎在后端运行)等 +- **控制**:`POST /api/display/command` → MQTT `opc/display/command` → 所有大屏订阅同步响应 +- **媒体**:上传/列表/播放列表/`/file` 静态返回(图片/视频统一由后端托管) +- **AI**:通义千问(DashScope)+ 工具调用(切页/控制/卡片/通知,经 MQTT 广播);语音识别用阿里云 paraformer + +## 启动 + +```bash +cd backend +uv sync # 安装依赖(fastapi/uvicorn/paho-mqtt/dashscope/jinja2 等) +cp .env.example .env # 按需修改(含 DashScope Key、MQTT 账号) +uv run python main.py # 或 .venv/bin/python main.py +``` + +启动后:API 与前端页面均在 `http://0.0.0.0:10085`(浏览器直接打开即大屏)。 + +## 环境变量(backend/.env) + +| 变量 | 说明 | +|---|---| +| `DPM_HOST` / `DPM_PORT`(或 `FASTAPI_HOST`/`FASTAPI_PORT`) | 监听地址,默认 `0.0.0.0:10085` | +| `DPM_MQTT_HOST` / `DPM_MQTT_PORT` | MQTT broker 地址,默认读 `MQTT_BROKER_HOST/PORT` | +| `MQTT_USERNAME` / `MQTT_PASSWORD` | **服务端发布账号:dpmserver / 123456** | +| `DASHSCOPE_API_KEY` | 阿里云 DashScope(LLM + 语音识别) | +| `DPM_LLM_MODEL` | 默认 `qwen-plus` | +| `DPM_ASR_MODEL` | 默认 `paraformer-realtime-v2` | + +> 前端连接配置在项目根 `.env.local`(已 gitignore):`VITE_API_BASE`、`VITE_MQTT_URL`、`VITE_MQTT_USERNAME=dpm`、`VITE_MQTT_PASSWORD=123456`。 + +## MQTT 主题 + +| 主题 | 方向 | 说明 | +|---|---|---| +| `opc/display/command` | 后端→前端 | `{cmd_id, ts, action, params}`,action: `navigate` / `navigate_rel` / `play` / `pause` / `next` / `prev` / `alert` / `show_card` / `set_mode` | +| `opc/dashboard/tick` | 后端→前端 | 数据快照(`{ts, snapshot}`) | +| `opc/display/ack` | 前端→后端 | 指令回执(预留) | + +## 控制示例 + +```bash +# 切换页面(数据大屏 / 数字孪生 / AI 助手 / 媒体轮播) +curl -X POST http://localhost:10085/api/display/command \ + -H "Content-Type: application/json" \ + -d '{"action":"navigate","params":{"page":"/twin"}}' + +# 媒体控制 +curl -X POST http://localhost:10085/api/display/command \ + -d '{"action":"next","params":{}}' + +# 大屏通知 +curl -X POST http://localhost:10085/api/display/command \ + -d '{"action":"alert","params":{"title":"园区通知","content":"..."}}' +``` + +## 关键接口 + +- `GET /api/dashboard/snapshot` — 大屏全量数据快照 +- `GET /api/park/companies` · `GET /api/park/zones` — 企业分布 +- `POST /api/ai/chat` — AI 对话(`{messages:[{role,content}]}` → `{reply, tools, mqtt_published}`) +- `POST /api/ai/asr` — 语音识别(multipart 上传录音 + `format` 参数) +- `GET/POST /api/playlist` · `POST /upload` · `GET /media` — 媒体与播放列表 +- `POST /api/display/command` — 管理端控制指令(转 MQTT 广播) +- `GET /api/events` — SSE 兼容通道(MQTT 不可用时的前端回退) + +## 说明 + +- 大屏数据由后端模拟引擎产生(`sim_engine.py`,与原前端 `parkData.js` 逻辑一致);前端离线时也有本地兜底 +- 媒体文件存放于 `backend/media/`,`/file/...` 直接返回 +- 安全:`backend/.env` 与根 `.env.local` 已加入 `.gitignore`,含阿里云密钥与 MQTT 凭据,勿提交 + + +## Docker 部署(推荐) + +一键构建并启动 **后端 + EMQX Broker**: + +```bash +# 1) (可选)配置部署参数 +export MQTT_PUBLIC_HOST=192.168.1.50 # 展播端可访问的服务器局域网 IP(Broker WebSocket) +export EMQX_USER=dpmserver +export EMQX_PASS=你的密码 +export DASHSCOPE_API_KEY=sk-xxx # AI/语音密钥(未配置时 AI 走本地规则引擎) +export DPM_S2S_ENABLED=1 # 实时语音对话开关 + +# 2) 构建并启动 +docker compose up -d --build + +# 3) 验证 +curl http://192.168.1.9:10085/api/health # {"ok":true,...} +docker compose logs -f backend +``` + +- 端口:`10085`(API/后台/媒体)、`8765`(s2s 语音)、`1883/8083/18083`(EMQX) +- 数据卷:`dpm_media`(媒体)、`dpm_knowledge`(知识库 park.md,可热更新)、`dpm_data`(设置/播放列表) +- 展播端(Tauri 大屏)启动时经 `GET /api/config` 自动获取 MQTT WebSocket 地址 +- 常用命令:`docker compose down` 停止;`docker compose up -d --build backend` 仅重建后端; + `docker compose ps` 查看状态 + +> 单独构建镜像:`docker build -f backend/Dockerfile -t dpm-backend .` diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/admin.py b/backend/app/admin.py new file mode 100644 index 0000000..e026051 --- /dev/null +++ b/backend/app/admin.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +"""管理后台页面(FastAPI + Jinja2 服务端渲染) + GET /admin → 登录页 或 管理台(按 Cookie 鉴权) + POST /admin/login → 表单登录,成功写入签名 Cookie + GET /admin/logout → 清除 Cookie + 媒体库 / 播放列表 / 设置均由 Jinja 服务端渲染,动态操作用少量 JS 调 REST +""" + +import hashlib +import hmac +import logging + +from fastapi import Request +from fastapi.responses import HTMLResponse, RedirectResponse +from fastapi.templating import Jinja2Templates +from pathlib import Path + +from .config import settings +from .storage import storage +from .routers import _list_media, _playlist_files + +log = logging.getLogger("dpm.admin") + +TEMPLATES_DIR = Path(__file__).resolve().parent.parent / "templates" +templates = Jinja2Templates(directory=str(TEMPLATES_DIR)) + + +# ---------------- 签名 Cookie ---------------- + +def _admin_token(): + return hmac.new(settings.ADMIN_SECRET.encode(), b"dpm-admin", hashlib.sha256).hexdigest() + + +def _verify_admin_cookie(cookie): + return bool(cookie) and hmac.compare_digest(cookie, _admin_token()) + + +def _set_admin_cookie(resp): + resp.set_cookie("dpm_admin", _admin_token(), max_age=7 * 86400, httponly=True, samesite="lax") + return resp + + +def _clear_admin_cookie(resp): + resp.delete_cookie("dpm_admin") + return resp + + +# ---------------- 路由 ---------------- + +async def admin_page(request: Request): + if not _verify_admin_cookie(request.cookies.get("dpm_admin")): + return templates.TemplateResponse( + request=request, + name="admin.html", + context={ + "logged_in": False, + "error": request.query_params.get("error") == "1", + }, + ) + + files = _list_media()["files"] + playlist = _playlist_files() + s = storage.get_settings() + return templates.TemplateResponse( + request=request, + name="admin.html", + context={ + "logged_in": True, + "error": False, + "files": files, + "playlist": playlist["files"], + "play_mode": s.get("play_mode", "sequential"), + "image_duration": s.get("image_duration", 5), + "volume": s.get("volume", 80), + "sfx_volume": s.get("sfx_volume", 60), + "fullscreen": s.get("fullscreen", True), + "autostart": s.get("autostart", False), + "from_screen": request.query_params.get("from") == "screen", + }, + ) + + +async def admin_login(request: Request): + form = await request.form() + s = storage.get_settings() + if form.get("username") == s.get("username") and form.get("password") == s.get("password"): + return _set_admin_cookie(RedirectResponse("/admin", status_code=303)) + return RedirectResponse("/admin?error=1", status_code=303) + + +async def admin_logout(): + return _clear_admin_cookie(RedirectResponse("/admin", status_code=303)) diff --git a/backend/app/ai_tools.py b/backend/app/ai_tools.py new file mode 100644 index 0000000..5edbe36 --- /dev/null +++ b/backend/app/ai_tools.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +"""AI 助手工具调用引擎 + 后端收到对话 → 规则引擎判断意图 → 产出文字回复 + 工具调用列表 + 工具执行时通过 MQTT 广播 opc/display/command,前端(所有大屏)同步响应 + 工具:navigate(切页) / control(媒体控制) / alert(通知) / show_card(展示卡片) +""" + +import json +import logging + +from .mqtt import hub + +log = logging.getLogger("dpm.ai_tools") + +# ---------------- 工具执行 ---------------- + +def execute_tool(tool, screen_id=""): + """执行一个工具调用:发布 MQTT 控制指令(带发起屏幕 screen_id,只发到该屏),返回是否发布成功""" + t = tool.get("type") + params = tool.get("params") or {} + if t == "navigate": + return hub.publish_command("navigate", {"page": params.get("page", "home")}, screen_id=screen_id).get("published", False) + if t == "control": + action = params.get("action") + if action in ("play", "pause", "next", "prev"): + return hub.publish_command(action, screen_id=screen_id).get("published", False) + if action == "set_mode": + return hub.publish_command("set_mode", {"mode": params.get("mode", "auto")}, screen_id=screen_id).get("published", False) + return False + if t == "alert": + return hub.publish_command("alert", {"title": params.get("title", "提示"), "content": params.get("content", "")}, screen_id=screen_id).get("published", False) + if t == "show_card": + return hub.publish_command("show_card", params, screen_id=screen_id).get("published", False) + return False + + +def run_tools(tools, screen_id=""): + """执行一组工具(带发起屏幕 screen_id),返回 [{type, ok}] 并汇总是否全部发布成功""" + results = [] + all_ok = True + for tool in tools or []: + ok = bool(execute_tool(tool, screen_id)) + log.info("ai_tools: 执行工具 %s params=%s screen=%s -> %s", tool.get("type"), + json.dumps(tool.get("params") or {}, ensure_ascii=False), screen_id or "all", ok) + results.append({"type": tool.get("type"), "ok": ok}) + all_ok = all_ok and ok + return results, all_ok + + +# ---------------- 意图 → 回复 + 工具 ---------------- + +PAGES = { + "home": "/", "数据大屏": "/", "首页": "/", + "twin": "/twin", "数字孪生": "/twin", "3d": "/twin", "孪生": "/twin", + "ai": "/ai", "ai助手": "/ai", "智能助手": "/ai", + "screen": "/screen", "媒体": "/screen", "轮播": "/screen", "媒体轮播": "/screen", +} + +RULES = [ + { + "match": ["入驻", "申请", "入孵", "入园", "如何加入", "怎样加入"], + "reply": "入驻流程(按《入驻管理规程》,每年原则上两次):\n\n1. 提出申请:提交《入驻申请表》《创业计划书》、身份证(外籍提供护照签证)、学历证明、营业执照、资质专利证书等材料\n2. 项目初审:服务机构初审,材料不齐一次性告知补全\n3. 入驻评审:从《昆明市就业创业导师库》抽取 3 名评审专家,路演答辩,量化打分 100 分制,80 分及以上通过\n4. 评审公示:报主管部门备案后在昆明市人社局官网公示\n5. 签署协议:签订《入驻孵化协议》,分配场地/工位,正式入驻\n\n按官方统计(2026年7月)园区实际入驻企业 38 家。咨询电话:18087174891。", + "tools": [{"type": "show_card", "params": {"card": "overview"}}], + }, + { + "match": ["政策", "补贴", "优惠", "扶持", "减免", "奖励", "资助"], + "reply": "园区政策服务覆盖「七位一体」中的政策维度:\n\n· 场地租金减免:符合条件的大学生创业项目可享受场地优惠\n· 创业补贴:协助对接省、市两级创业补贴与一次性创业资助\n· 税费优惠:对接小微企业所得税减免、增值税优惠等政策\n· 项目申报:协助高新技术企业认定、科技型中小企业备案申报辅导\n\n更多政策细节可咨询园区政策服务窗口(电话:18087174891)。", + "tools": [], + }, + { + "match": ["场地", "办公室", "工位", "租金", "收费", "面积", "多少钱", "价格"], + "reply": "园区建筑面积约 3000 ㎡(官方统计口径),可容纳约 50 个创业团队,按发展阶段设置四大孵化区域(《宣传册》):\n\n· OPC创业空间:AI技术支撑、数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域,拎包入驻,最长 2 年\n· 创业苗圃区:创意阶段未注册团队,开放式共享工位,最长 2 年\n· 孵化加速区:已注册初创企业,独立办公空间,基础 2 年可延 1 年(累计≤3年)\n· 国际创客区:归国/外籍留学生,个性化办公空间,参照加速区\n\n办公场地免费提供(含桌椅、文件柜、高速网络等基础配套),水电网络全配套、拎包入驻。", + "tools": [{"type": "show_card", "params": {"card": "zones"}}], + }, + { + "match": ["企业", "公司", "名录", "名单", "入驻企业", "有哪些企业", "查企业"], + "reply": "按官方统计(2026年7月),园区实际入驻企业 38 家;《入驻企业信息表》收录本期入驻企业(项目)名录 39 家,包括云南派音人工智能科技(PineSound)、米勒克尔蓝宝石珠宝、云南宸中低空经济、仰光客厅、中越生物医疗、酷享野农AI农业、研X同行者网络等。\n\n行业分布:人工智能 7 家、跨境电商 5 家、教育培训 4 家、文旅康养 4 家、文化创意 4 家、生物医药 4 家、低空经济 3 家、现代农业 3 家、绿色环保 2 家、企业服务 2 家、新材料 1 家。\n\n我已在屏幕上为您展示企业分布卡片。", + "tools": [{"type": "show_card", "params": {"card": "companies"}}], + }, + { + "match": ["融资", "投资", "资金", "贷款", "估值", "资本"], + "reply": "园区融资服务包含:\n\n· 融资对接:定期组织路演与投资机构对接活动\n· 经营成效:入驻团队累计经营收入约 2.19 亿元(省级绩效累计口径,截至2026年5月)\n· 金融支持:协助对接银行创业贷款、政府创业担保贷款\n· 财务辅导:提供融资方案设计与商业计划书辅导", + "tools": [], + }, + { + "match": ["就业", "招聘", "岗位", "人才", "用工"], + "reply": "园区就业数据(口径说明):\n\n· 官方统计(2026年7月):当年累计带动就业 213 人\n· 省级绩效累计口径(截至2026年5月):累计带动就业 1,529 人(本年度 106 人)\n\n· 企业招聘:在驻企业持续发布岗位\n· 人才对接:与在昆高校共建实习实训基地\n· 创业带动:园区鼓励以创业带动就业", + "tools": [], + }, + { + "match": ["导师", "培训", "课程", "辅导", "孵化服务", "孵化器"], + "reply": "园区组建创业指导专家团队 50 人,提供全周期孵化服务:\n\n· 一对一辅导:创业导师定期坐诊,覆盖商业模式、法律财税、市场营销\n· 培训课程:创业训练营、行业专题沙龙、政策解读会\n· 资源对接:技术、供应链、渠道等产业资源链接\n· 公共服务:工商注册、知识产权、法律咨询一站式代办", + "tools": [], + }, + { + "match": ["AI", "模型", "算力", "tokens", "token", "工具", "智能", "赋能", "大模型"], + "reply": "园区 AI 赋能服务是「七位一体」的核心特色:\n\n· 大模型推理:接入 DeepSeek、通义千问、智谱 GLM-4、豆包、讯飞星火等多模型\n· 工具调用:文档生成、数据查询、图像创作、代码执行、语音合成等智能工具\n· 算力与成本:实时提供 token 消耗统计与速率监控\n\n今日园区 AI 推理任务持续运行,服务节点 12/39 在线。", + "tools": [], + }, + { + "match": ["加速区", "国际区", "成长区", "分区", "区域", "区别", "布局"], + "reply": "园区按创业企业(项目)发展阶段设置四大孵化区域(《宣传册》《入驻管理规程》):\n\n· OPC创业空间:AI技术支撑、轻资产领域,拎包入驻,孵化周期最长 2 年\n· 创业苗圃区:创意阶段未注册团队,共享工位,最长 2 年\n· 孵化加速区:已注册初创企业,独立办公,基础 2 年可延 1 年(累计≤3年)\n· 国际创客区:归国/外籍留学生,个性化空间,参照加速区\n\n我已在屏幕上为您展示分区介绍卡片。", + "tools": [{"type": "show_card", "params": {"card": "zones"}}], + }, + { + "match": ["切", "去", "打开", "显示", "进入"], + "reply": None, # 由下方页面/控制规则补充 + }, + { + "match": ["你好", "您好", "hi", "hello", "嗨", "在吗"], + "reply": "您好!我是 OPC 园区 AI 智能助手。\n\n可以问我:入驻流程、政策扶持、场地收费、AI 赋能、企业名录、融资服务等问题;也可以让我「切换到数字孪生页」「打开媒体轮播」或「展示企业卡片」。", + "tools": [], + }, + { + "match": ["谢谢", "感谢", "好的", "明白"], + "reply": "不客气,随时为您服务。您也可以让我展示园区数据卡片,或切换到任意展示页面。", + "tools": [], + }, +] + +DEFAULT_REPLY = "园区提供「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体服务。\n\n按官方统计(2026年7月):实际入驻企业 38 家,当年累计带动就业 213 人,当年生产经营总额 252.15 万元;省级绩效累计口径(截至2026年5月):累计孵化企业 181 家、累计带动就业 1,529 人、累计经营收入约 2.19 亿元。\n\n您可以这样问我:\n· 如何申请入驻园区?\n· 展示入驻企业卡片\n· 切换到数字孪生页面" + + +def _detect_page(text): + for key, path in PAGES.items(): + if key in text: + return path + return None + + +def _detect_control(text): + if any(k in text for k in ["播放", "继续播放", "开始播放"]): + return "play" + if any(k in text for k in ["暂停", "停止播放"]): + return "pause" + if any(k in text for k in ["下一个", "下一张", "下一屏", "切换下一"]): + return "next" + if any(k in text for k in ["上一个", "上一张", "上一屏", "切换上一"]): + return "prev" + return None + + +def run_chat(messages): + """输入对话消息列表([{"role","content"}]),返回 {reply, tools}""" + text = "" + for m in reversed(messages or []): + if m.get("role") in ("user", "me"): + text = (m.get("content") or "").strip() + break + + reply = None + tools = [] + + # 1) 页面切换意图(优先级最高) + page = _detect_page(text) + if page: + label = {"/": "数据大屏", "/twin": "数字孪生", "/ai": "AI 助手", "/screen": "媒体轮播"}.get(page, page) + reply = f"好的,已为您切换到「{label}」页面。" + tools = [{"type": "navigate", "params": {"page": page}}] + return {"reply": reply, "tools": tools} + + # 2) 媒体控制意图 + ctrl = _detect_control(text) + if ctrl: + names = {"play": "播放", "pause": "暂停", "next": "切换下一项", "prev": "切换上一项"} + reply = f"好的,已执行:{names[ctrl]}。" + tools = [{"type": "control", "params": {"action": ctrl}}] + return {"reply": reply, "tools": tools} + + # 3) 知识库规则 + for rule in RULES: + if any(k in text for k in rule["match"]): + reply = rule["reply"] + tools = rule["tools"] + break + + if reply is None: + reply = DEFAULT_REPLY + + # 工具已在后端通过 MQTT 广播;返回值供请求端本地同步执行 + return {"reply": reply, "tools": tools} diff --git a/backend/app/asr.py b/backend/app/asr.py new file mode 100644 index 0000000..b6be959 --- /dev/null +++ b/backend/app/asr.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +"""语音识别 —— 阿里云 DashScope paraformer-realtime-v2(流式) + 前端录音上传(m4a/wav/ogg/opus/pcm)→ 这里转写为文本 +""" + +import logging +import os +import time + +import certifi + +# macOS 系统 Python 无系统 CA:必须在导入 dashscope 之前设置, +# aiohttp 才会使用 certifi 根证书(否则 WS 连接报 SSL 证书错误) +os.environ.setdefault("SSL_CERT_FILE", certifi.where()) + +import dashscope # noqa: E402 +from dashscope.audio.asr import Recognition, RecognitionCallback # noqa: E402 + +from .config import settings # noqa: E402 + +log = logging.getLogger("dpm.asr") + + +def _err_text(result): + """安全提取 RecognitionResult 的错误描述(其 __str__ 有缺陷)""" + for attr in ("message", "code"): + v = getattr(result, attr, None) + if v: + return f"{attr}={v}" + return repr(result) + +# 前端 MediaRecorder 可能产生的容器/编码 → paraformer 格式名 +FORMAT_ALIASES = { + "mp4": "m4a", "m4a": "m4a", "aac": "m4a", + "webm": "ogg", "opus": "opus", "ogg": "ogg", + "wav": "wav", "pcm": "pcm", "mp3": "mp3", +} + +# m4a/mp3 等压缩格式采样率由服务端自动识别 +_AUTO_SAMPLE_FORMATS = {"m4a", "mp3", "ogg", "opus", "wav"} + + +class _Callback(RecognitionCallback): + def __init__(self): + self.sentences = [] + self.error = None + + def on_message(self, message): + try: + header = message.get("header", {}) + if header.get("action") != "result": + return + payload = message.get("payload", {}) + sentences = payload.get("sentence", {}).get("sentences") or [] + for s in sentences: + if s.get("sentence_end"): + self.sentences.append(s.get("text", "")) + except Exception as e: # noqa: BLE001 + log.warning("ASR 消息解析异常: %s", e) + + def on_error(self, result): + self.error = result + log.warning("ASR 错误: %s", _err_text(result)) + + +def transcribe(audio_bytes: bytes, fmt: str = "m4a") -> str: + """上传音频字节 → 返回转写文本(空串表示未识别到内容)""" + if not settings.DASHSCOPE_API_KEY: + raise RuntimeError("未配置 DASHSCOPE_API_KEY,无法使用阿里云语音识别") + + dashscope.api_key = settings.DASHSCOPE_API_KEY + fmt = FORMAT_ALIASES.get((fmt or "m4a").lower().lstrip("."), "m4a") + + cb = _Callback() + rec = Recognition( + model=settings.ASR_MODEL, + format=fmt, + sample_rate=16000 if fmt in ("pcm", "wav") else 0, # 压缩格式自动识别 + callback=cb, + ) + + # 分块发送,模拟流式(适当间隔,避免触发服务端 batching 报错) + rec.start() + chunk = 16 * 1024 + for i in range(0, len(audio_bytes), chunk): + rec.send_audio_frame(audio_bytes[i:i + chunk]) + time.sleep(0.08) + time.sleep(0.5) # 等待尾部识别 + rec.stop() + + if cb.error: + raise RuntimeError(f"阿里云语音识别失败: {_err_text(cb.error)}") + return "".join(cb.sentences).strip() diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..ff4f8e7 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +"""OPC 智能园区后端 —— 全局配置(backend/.env 可覆盖,未配置时用默认值)""" + +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent # backend/ + + +def _load_dotenv(path): + """极简 .env 解析:KEY=VALUE,支持 # 注释与引号""" + if not Path(path).exists(): + return + for raw in Path(path).read_text("utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip('"').strip("'") + if key and key not in os.environ: + os.environ[key] = value + + +_load_dotenv(BASE_DIR / ".env") + + +def _env(key, default): + return os.getenv(key, default) + + +class Settings: + # ---- HTTP ---- + HOST = _env("DPM_HOST", _env("FASTAPI_HOST", "0.0.0.0")) + PORT = int(_env("DPM_PORT", _env("FASTAPI_PORT", "8000"))) + + # ---- 数据目录 ---- + MEDIA_DIR = Path(_env("DPM_MEDIA_DIR", str(BASE_DIR / "media"))) + DATA_FILE = Path(_env("DPM_DATA_FILE", str(BASE_DIR / "data.json"))) + DIST_DIR = Path(_env("DPM_DIST_DIR", str(BASE_DIR.parent / "dist"))) # 前端构建产物(可选托管) + + # ---- MQTT(优先使用 .env 中的局域网 Broker 配置) ---- + MQTT_ENABLED = _env("DPM_MQTT_ENABLED", "1") == "1" + MQTT_HOST = _env("DPM_MQTT_HOST", _env("MQTT_BROKER_HOST", "192.168.1.9")) + MQTT_PORT = int(_env("DPM_MQTT_PORT", _env("MQTT_BROKER_PORT", "1883"))) + MQTT_USERNAME = _env("DPM_MQTT_USERNAME", _env("MQTT_USERNAME", "")) or None + MQTT_PASSWORD = _env("DPM_MQTT_PASSWORD", _env("MQTT_PASSWORD", "")) or None + MQTT_CLIENT_ID = _env("DPM_MQTT_CLIENT_ID", "dpm-backend") + # 前端 WebSocket 接入地址(默认按 .env 的 broker 主机 + EMQX 常规 ws 端口) + MQTT_WS_URL = _env( + "DPM_MQTT_WS", + f"ws://{_env('MQTT_PUBLIC_HOST', _env('MQTT_BROKER_HOST', 'localhost'))}:8083/mqtt", + ) + MQTT_TICK_INTERVAL = float(_env("DPM_MQTT_TICK_INTERVAL", "2.2")) + + # ---- 主题 ---- + TOPIC_COMMAND = _env("DPM_TOPIC_COMMAND", "opc/display/command") + TOPIC_TICK = _env("DPM_TOPIC_TICK", "opc/dashboard/tick") + TOPIC_ACK = _env("DPM_TOPIC_ACK", "opc/display/ack") + TOPIC_HEARTBEAT = _env("DPM_TOPIC_HEARTBEAT", "opc/display/heartbeat") + SCREEN_TTL = float(_env("DPM_SCREEN_TTL", "30")) # 大屏心跳过期秒数 + + # ---- 阿里云 DashScope(LLM + 语音识别) ---- + DASHSCOPE_API_KEY = _env("DASHSCOPE_API_KEY", "") + LLM_MODEL = _env("DPM_LLM_MODEL", "qwen-plus") # 通义千问 + ASR_MODEL = _env("DPM_ASR_MODEL", "paraformer-realtime-v2") # 流式语音识别 + LLM_BASE_URL = _env("DPM_LLM_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1") + ASR_TIMEOUT = float(_env("DPM_ASR_TIMEOUT", "30")) + + # ---- 管理后台 ---- + ADMIN_SECRET = _env("DPM_ADMIN_SECRET", "dpm-admin-secret-change-me") + + # ---- s2s 实时语音栈(VAD本地 + 云ASR/LLM/TTS) ---- + S2S_ENABLED = _env("DPM_S2S_ENABLED", "0") == "1" + S2S_HOST = _env("DPM_S2S_HOST", "0.0.0.0") + S2S_PORT = int(_env("DPM_S2S_PORT", "8765")) + S2S_NUM_PIPELINES = int(_env("DPM_S2S_NUM_PIPELINES", "1")) + VISION_LLM_MODEL = _env("DPM_VISION_LLM_MODEL", "qwen3-vl-flash") # 对话开场画面识别(多模态) + S2S_STT_MODEL = _env("DPM_S2S_STT_MODEL", "qwen3-asr-flash-realtime") + S2S_LLM_MODEL = _env("DPM_S2S_LLM_MODEL", "qwen-plus") + S2S_TTS_MODEL = _env("DPM_S2S_TTS_MODEL", "qwen3-tts-flash-realtime") + S2S_TTS_VOICE = _env("DPM_S2S_TTS_VOICE", "Cherry") + S2S_BUILD_RETRIES = int(_env("DPM_S2S_BUILD_RETRIES", "4")) + S2S_BUILD_RETRY_DELAY = float(_env("DPM_S2S_BUILD_RETRY_DELAY", "10")) + S2S_LOG_LEVEL = _env("DPM_S2S_LOG_LEVEL", "INFO").upper() + # 对外可访问的 s2s 实时语音 WS 地址(供 /api/config 下发;DPM_VOICE_WS 可显式覆盖) + VOICE_WS = _env("DPM_VOICE_WS", "") # 显式指定时优先使用(s2s 独立进程场景) + S2S_WS_URL = ( + VOICE_WS + or f"ws://{_env('MQTT_PUBLIC_HOST', _env('MQTT_BROKER_HOST', 'localhost'))}:{S2S_PORT}/v1/realtime" + ) + # 语音助手基础提示词(.env 设 DPM_S2S_INSTRUCTIONS 可覆盖;知识库内容由 rag 自动拼接) + S2S_INSTRUCTIONS = _env( + "DPM_S2S_INSTRUCTIONS", + "你是昆小创,昆明市大学生创业园的专属智能语音助手。可用工具:get_park_overview(园区实时数据)、" + "query_companies(企业名录)、control_display(大屏控制)、get_time(时间)。" + "涉及园区数据/企业/大屏控制时务必调用工具获取准确信息。请用简洁专业的中文回答,不超过三句话。", + ) + + + +settings = Settings() diff --git a/backend/app/event_bus.py b/backend/app/event_bus.py new file mode 100644 index 0000000..6b0f054 --- /dev/null +++ b/backend/app/event_bus.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +"""SSE 事件总线 —— 兼容旧通道(前端 MQTT 不可用时回退) + 所有 MQTT 控制指令同时推送到 SSE /api/events +""" + +import asyncio +import json + + +class EventBus: + def __init__(self): + self._queues = set() + self._loop = None + + def set_loop(self, loop): + self._loop = loop + + def subscribe(self): + q = asyncio.Queue(maxsize=256) + self._queues.add(q) + return q + + def unsubscribe(self, q): + self._queues.discard(q) + + def emit(self, payload: dict): + """线程安全:从任意线程广播事件""" + if not self._loop: + return + try: + asyncio.run_coroutine_threadsafe(self._emit(payload), self._loop) + except Exception: # noqa: BLE001 + pass + + async def _emit(self, payload): + data = json.dumps(payload, ensure_ascii=False) + for q in list(self._queues): + try: + q.put_nowait(data) + except asyncio.QueueFull: + pass + + +bus = EventBus() diff --git a/backend/app/llm.py b/backend/app/llm.py new file mode 100644 index 0000000..08829d6 --- /dev/null +++ b/backend/app/llm.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +"""AI 对话引擎 —— 阿里云 DashScope(通义千问)+ 函数调用 + 工具调用执行后通过 MQTT 广播,所有大屏同步响应 + DashScope 不可用时自动回退本地规则引擎(离线兜底) +""" + +import json +import logging +import ssl +import urllib.error +import urllib.request + +import certifi + +from .config import settings + +log = logging.getLogger("dpm.llm") + + +def _trunc(s, n=400): + """日志用截断:超长内容只留前 n 字并标注总长。""" + s = str(s) + return s if len(s) <= n else s[:n] + f"...(共{len(s)}字)" + + +# macOS 系统 Python 无系统 CA,使用 certifi 提供的根证书 +_SSL_CTX = ssl.create_default_context(cafile=certifi.where()) + +SYSTEM_PROMPT = ( + "你是「昆明市大学生创业园 · OPC 智能园区」的 AI 智能助手,运行在大屏展播系统上。" + "园区提供空间、孵化、融资、政策、资源、AI 赋能、综合服务七位一体服务。" + "回答园区相关问题时,必须严格依据【园区知识库】中的标准资料口径回答," + "不得使用资料外的数字;涉及运营数据时请区分口径(官方统计/省级绩效累计/AI平台指标)," + "并在需要时调用 get_park_overview 工具获取实时准确数据。" + "当可能需要「切换页面 / 控制播放 / 展示卡片 / 弹出通知」时,必须调用对应工具;" + "其余园区相关问题用简洁、专业的中文回答,可适当使用列表。" + "你必须基于知识库或者工具调用结果回答问题,不能编造数据或内容。" + "如果有不确定的内容,请明确告知用户「我不确定,请咨询园区工作人员」。" + "如果用户询问园区的企业,没有指定的情况下,必须介绍派音人工智能" + "企业信息(企业名称、负责人、入驻分区、简介)一律以【园区知识库·检索命中】中企业名录原文为准;" + "严禁自行编造或补充企业备案号、成立时间、注册资本、投资方、获奖、政府项目等知识库未记载的细节;" + "若用户问到的企业细节在知识库中没有记载,请如实说明「该信息在园区资料中未记载」,不要编造。" +) + +# 暴露给大模型的工具定义(执行时经 MQTT 广播到前端) +TOOLS = [ + { + "type": "function", + "function": { + "name": "navigate_page", + "description": "切换大屏展示页面(数据大屏 / 数字孪生 / AI 助手 / 媒体轮播)", + "parameters": { + "type": "object", + "properties": { + "page": {"type": "string", "enum": ["/", "/twin", "/ai", "/screen"], + "description": "目标页面路径"}, + }, + "required": ["page"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "media_control", + "description": "控制媒体播放(播放/暂停/下一项/上一项)", + "parameters": { + "type": "object", + "properties": { + "action": {"type": "string", "enum": ["play", "pause", "next", "prev"]}, + }, + "required": ["action"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "show_card", + "description": "在大屏上展示信息卡片(企业分布 / 分区介绍 / 园区总览 / 自定义内容)", + "parameters": { + "type": "object", + "properties": { + "card": {"type": "string", "enum": ["companies", "zones", "overview", "custom"]}, + "title": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["card"], + }, + }, + }, + { + "type": "function", + "function": { + "name": "send_alert", + "description": "在大屏上弹出通知提示", + "parameters": { + "type": "object", + "properties": { + "title": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["title", "content"], + }, + }, + }, +] + +_TOOL_MAP = { + "navigate_page": "navigate", + "media_control": "control", + "show_card": "show_card", + "send_alert": "alert", +} + + +def _chat_once(messages, with_tools=True): + log.info("llm: 调用模型 %s(%d 条消息, 带工具=%s)", settings.LLM_MODEL, len(messages), with_tools) + payload = { + "model": settings.LLM_MODEL, + "messages": messages, + "temperature": 0.6, + } + if with_tools: + payload["tools"] = TOOLS + req = urllib.request.Request( + f"{settings.LLM_BASE_URL}/chat/completions", + data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}", + }, + ) + with urllib.request.urlopen(req, timeout=60, context=_SSL_CTX) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def _fallback(messages, screen_id=""): + """DashScope 不可用时回退本地规则引擎""" + from .ai_tools import run_chat as rule_run + + result = rule_run(messages) + # 规则引擎产出的工具同样执行(MQTT 广播,带发起屏幕),并汇总发布结果 + _, all_ok = _exec_all(result.get("tools", []), screen_id) + result["model"] = "rule-engine" + result["mqtt_published"] = all_ok + return result + + +def _exec_all(tools, screen_id=""): + """执行工具列表(带发起屏幕 screen_id,工具结果只发到该屏),返回 (results, all_ok)""" + from .ai_tools import run_tools + + results, all_ok = run_tools(tools, screen_id) + return results, all_ok + + +def run_chat(messages, screen_id=""): + """入口:{reply, tools, model} + tools 已在后端执行(MQTT 广播,带发起屏幕 screen_id),返回值供请求端本地同步执行""" + if not settings.DASHSCOPE_API_KEY: + log.warning("未配置 DASHSCOPE_API_KEY,使用本地规则引擎") + return _fallback(messages, screen_id) + + # 注入园区知识库(标准资料口径):按用户 query 检索命中段落动态注入, + # 替代原 1200 字截断全量注入(避免尾部知识丢失)。无命中时回退 brief()。 + system = SYSTEM_PROMPT + kb = "" + retrieved_count = 0 + try: + from .rag import brief, retrieve + query = "" + for m in reversed(messages or []): + if m.get("role") in ("user", "me"): + query = (m.get("content") or "").strip() + break + hits = retrieve(query, top_k=4) if query else [] + if hits: + retrieved_count = len(hits) + kb = "\n\n".join(f"[{i + 1}] {c}" for i, c in enumerate(hits)) + log.info("llm: 知识库检索命中 %d 段(query=%s)", len(hits), _trunc(query, 120)) + else: + kb = brief() + if kb: + system += f"\n\n【园区知识库 · 检索命中】\n{kb}" + except Exception as e: # noqa: BLE001 + log.warning("llm: 知识库注入失败(仅用基础提示词): %s", e) + + log.info("llm: ── 对话开始(%d 条输入消息)──", len(messages or [])) + log.info("llm: 系统提示词=%s", _trunc(system, 700)) + if kb: + log.info("llm: 注入知识库内容=%s", _trunc(kb, 500)) + for m in (messages or [])[-5:]: + log.info("llm: 输入[%s] %s", m.get("role"), _trunc(m.get("content", ""), 300)) + + msgs = [{"role": "system", "content": system}] + [ + {"role": m.get("role") == "me" and "user" or m.get("role", "user"), "content": m.get("content", "")} + for m in (messages or []) + ] + + try: + data = _chat_once(msgs) + choice = data["choices"][0]["message"] + reply = choice.get("content") or "" + tool_calls = choice.get("tool_calls") or [] + executed = [] + + if tool_calls: + log.info("llm: 模型请求调用 %d 个工具:", len(tool_calls)) + for tc in tool_calls: + fn = tc.get("function", {}) + log.info("llm: 工具 %s args=%s", fn.get("name", ""), _trunc(fn.get("arguments", ""), 300)) + for tc in tool_calls: + fn = tc.get("function", {}) + name = fn.get("name", "") + try: + args = json.loads(fn.get("arguments") or "{}") + except json.JSONDecodeError: + args = {} + tool = {"type": _TOOL_MAP.get(name, name), "params": args} + executed.append(tool) + msgs.append({"role": "assistant", "content": None, "tool_calls": tool_calls}) + msgs.append({ + "role": "tool", + "tool_call_id": tc.get("id", ""), + "content": json.dumps({"ok": True}, ensure_ascii=False), + }) + # 工具执行(MQTT 广播),并汇总发布结果 + results, all_ok = _exec_all(executed, screen_id) + log.info("llm: 工具执行结果 %s", results) + for t in executed: + log.info("llm: 执行 %s %s", t["type"], json.dumps(t["params"], ensure_ascii=False)) + # 二次调用:携带工具结果生成最终回复 + try: + data2 = _chat_once(msgs, with_tools=False) + reply = data2["choices"][0]["message"].get("content") or reply + except Exception as e: # noqa: BLE001 + log.warning("LLM 二次调用失败(保留工具回复): %s", e) + else: + all_ok = True + + if not reply: + reply = "已完成操作。您还可以让我切换页面、控制播放或展示园区卡片。" + + log.info("llm: 最终回复=%s", _trunc(reply, 600)) + log.info("llm: ── 对话结束 ──") + return { + "reply": reply, + "tools": executed, + "model": settings.LLM_MODEL, + "mqtt_published": all_ok, + "retrieved": retrieved_count > 0, + "retrieved_count": retrieved_count, + "tool_names": [t.get("type") for t in executed if t.get("type")], + } + except urllib.error.HTTPError as e: + log.warning("DashScope HTTP %s: %s", e.code, e.read()[:300]) + except Exception as e: # noqa: BLE001 + log.warning("DashScope 调用失败,回退规则引擎: %s", e) + + return _fallback(messages) diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..f5e659e --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +"""OPC 智能园区后端 —— FastAPI 入口 + 启动:uvicorn app.main:app --host 0.0.0.0 --port 8000 + 或:python main.py +""" + +import asyncio +import logging +import re +import threading +import time +from contextlib import asynccontextmanager +from pathlib import Path + +from fastapi import FastAPI, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import FileResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +from .admin import admin_login, admin_logout, admin_page +from .config import settings +from .event_bus import bus +from .mqtt import hub +from .routers import router +from .sim_engine import sim_engine +from .s2s_bridge import start_s2s_backend + +logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s %(message)s") +log = logging.getLogger("dpm.main") + +DIST = Path(settings.DIST_DIR) + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # 启动准备 + settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True) + bus.set_loop(asyncio.get_running_loop()) + hub.start() + + stop = threading.Event() + + # s2s 实时语音栈(可选):后台线程构建管线池并起 RealtimeServer(:8765) + if settings.S2S_ENABLED: + start_s2s_backend(stop) + + def tick_loop(): + """数据引擎每 2.2s 推进一次;MQTT 连接时同步推送快照""" + while not stop.is_set(): + try: + snap = sim_engine.tick() + hub.publish_tick(snap) + except Exception as e: # noqa: BLE001 + log.warning("数据 tick 异常: %s", e) + stop.wait(settings.MQTT_TICK_INTERVAL) + + threading.Thread(target=tick_loop, daemon=True).start() + log.info("OPC 智能园区后端启动于 http://%s:%s", settings.HOST, settings.PORT) + if settings.MQTT_ENABLED: + log.info("MQTT Broker: %s:%s(前端 WS: %s)", settings.MQTT_HOST, settings.MQTT_PORT, settings.MQTT_WS_URL) + try: + yield + finally: + stop.set() + hub.stop() + log.info("OPC 智能园区后端已停止") + + +app = FastAPI(title="OPC 智能园区后端", version="1.0.0", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.middleware("http") +async def no_cache_admin_static(request: Request, call_next): + """管理后台页面与静态资源禁用缓存,避免旧 JS/CSS 残留导致按钮无响应""" + response = await call_next(request) + if request.url.path.startswith(("/static/", "/admin")): + response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate" + response.headers["Pragma"] = "no-cache" + response.headers["Expires"] = "0" + return response + +app.include_router(router) + +# 媒体资源静态服务(上传/播放的文件) +settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True) +app.mount("/file", StaticFiles(directory=str(settings.MEDIA_DIR)), name="media") + +# 管理后台静态资源(admin.css / admin.js) +_STATIC_DIR = Path(__file__).resolve().parent.parent / "static" +app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static") + +# 管理后台(FastAPI + Jinja2 服务端渲染)—— 必须在 SPA 回退之前注册 +app.add_api_route("/admin", admin_page, methods=["GET"], include_in_schema=False) +app.add_api_route("/admin/login", admin_login, methods=["POST"], include_in_schema=False) +app.add_api_route("/admin/logout", admin_logout, methods=["GET"], include_in_schema=False) + + +# ---------- 客户端安装包下载(必须在 SPA 回退之前注册) ---------- + +_DOWNLOAD_ROOT = Path(__file__).resolve().parent.parent # backend/ + + +@app.get("/download", include_in_schema=False) +@app.get("/11", include_in_schema=False) +async def download_installer(): + """下载 Windows 客户端安装包(backend/ 下 *.setup.exe,如 云超服..._x64-setup.exe)。""" + for f in sorted(_DOWNLOAD_ROOT.glob("*setup.exe")): + if f.is_file(): + return FileResponse( + path=str(f), + filename=f.name, + media_type="application/octet-stream", + ) + return JSONResponse({"detail": "安装包不存在"}, status_code=404) + + +# ---------- 客户端更新检测(仅 Windows:比对版本 → 下发安装包下载地址) ---------- + +def _latest_installer(): + """返回 backend/ 下最新(按文件名排序取最大)的 *.setup.exe。""" + for f in sorted(_DOWNLOAD_ROOT.glob("*setup.exe"), reverse=True): + if f.is_file(): + return f + return None + + +def _version_from_name(name: str) -> str: + """从安装包文件名解析版本,如 `..._0.1.0_x64-setup.exe` → `0.1.0`。""" + m = re.search(r"_(\d+\.\d+(?:\.\d+)?)", name or "") + return m.group(1) if m else "" + + +def _version_key(v: str): + return tuple(int(x) for x in v.split(".") if x.isdigit()) + + +class UpdateCheckBody(BaseModel): + current: str = "" # 前端上报的当前版本,如 0.1.0 + + +@app.post("/api/update/check", include_in_schema=False) +async def update_check(body: UpdateCheckBody): + """检测更新:返回最新版本号、安装包下载地址、是否需要更新(仅 Windows)。""" + inst = _latest_installer() + if not inst: + return {"ok": False, "error": "installer not found"} + latest = _version_from_name(inst.name) + current = (body.current or "").strip() + update_available = False + if latest and current and current != latest: + update_available = _version_key(latest) > _version_key(current) + elif latest and not current: + update_available = True + return { + "ok": True, + "latest_version": latest, + "installer_name": inst.name, + "download_url": "/download", + "update_available": update_available, + } + + +# ---------- 前端静态托管 + SPA 回退(浏览器直接访问 :8000 即可) ---------- + +@app.get("/{path:path}", include_in_schema=False) +async def spa_fallback(path: str): + if not DIST.exists(): + return JSONResponse({"detail": "前端未构建(dist 目录不存在)"}, status_code=404) + dist = DIST.resolve() + if not path: + target = dist / "index.html" + else: + target = (dist / path).resolve() + if target.is_file() and target.is_relative_to(dist): + return FileResponse(target) + index = dist / "index.html" + if index.exists(): + return FileResponse(index) + return JSONResponse({"detail": "Not Found"}, status_code=404) diff --git a/backend/app/mqtt.py b/backend/app/mqtt.py new file mode 100644 index 0000000..81c5690 --- /dev/null +++ b/backend/app/mqtt.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +"""MQTT 发布/订阅中心 —— 后端 ↔ 前端控制通道 + 发布:opc/display/command(页面/媒体/卡片控制)、opc/dashboard/tick(数据快照) + 订阅:opc/display/heartbeat(大屏在线心跳,统计在线大屏数) +""" + +import json +import logging +import os +import threading +import time +import uuid + +import paho.mqtt.client as mqtt + +from .config import settings +from .event_bus import bus + +log = logging.getLogger("dpm.mqtt") + + +def new_cmd_id(): + return uuid.uuid4().hex[:12] + + +def _unique_client_id(base): + """每个客户端进程使用唯一 client_id,避免与其它实例竞争被 broker 踢下线""" + return f"{base}-{os.getpid()}-{uuid.uuid4().hex[:6]}" + + +class MqttHub: + def __init__(self): + self.client = None + self.connected = False + self._lock = threading.RLock() + self.devices = {} # device_id(client_id) -> {role, parent_id, last_seen} + self.last_command = None # {action, params, ts, published} + + # ---------- 生命周期 ---------- + def start(self): + if not settings.MQTT_ENABLED: + log.info("MQTT 已禁用(DPM_MQTT_ENABLED=0)") + return + try: + self.client = mqtt.Client( + client_id=_unique_client_id(settings.MQTT_CLIENT_ID), + clean_session=True, + protocol=mqtt.MQTTv311, + ) + if settings.MQTT_USERNAME: + self.client.username_pw_set(settings.MQTT_USERNAME, settings.MQTT_PASSWORD) + self.client.on_connect = self._on_connect + self.client.on_disconnect = self._on_disconnect + self.client.on_message = self._on_message + self.client.connect_async(settings.MQTT_HOST, settings.MQTT_PORT, keepalive=30) + self.client.loop_start() + log.info("MQTT 连接中 %s:%s ...", settings.MQTT_HOST, settings.MQTT_PORT) + except Exception as e: # noqa: BLE001 + log.warning("MQTT 初始化失败(数据 API 不受影响): %s", e) + + def stop(self): + if self.client: + try: + self.client.loop_stop() + self.client.disconnect() + except Exception: # noqa: BLE001 + pass + self.client = None + + def _on_connect(self, client, userdata, flags, rc): + if rc == 0: + self.connected = True + log.info("MQTT 已连接 %s:%s", settings.MQTT_HOST, settings.MQTT_PORT) + client.subscribe(settings.TOPIC_HEARTBEAT, qos=0) + else: + log.warning("MQTT 连接失败 rc=%s", rc) + + def _on_disconnect(self, client, userdata, rc): + self.connected = False + if rc != 0: + log.warning("MQTT 断开(rc=%s),自动重连中...", rc) + + def _on_message(self, client, userdata, msg): + """接收大屏心跳:更新设备表在线时间(设备由注册表登记 role/parent)""" + if msg.topic == settings.TOPIC_HEARTBEAT: + try: + payload = json.loads(msg.payload.decode("utf-8")) + cid = payload.get("client_id") or msg.topic + with self._lock: + cur = self.devices.get(cid, {"role": "", "parent_id": ""}) + cur["last_seen"] = time.time() + self.devices[cid] = cur + self._prune() + except Exception: # noqa: BLE001 + pass + + def _prune(self): + """剔除超时未上报的设备""" + cutoff = time.time() - settings.SCREEN_TTL + self.devices = {k: v for k, v in self.devices.items() if v.get("last_seen", 0) > cutoff} + + # ---------- 状态查询 ---------- + def screens_online(self): + with self._lock: + return sum(1 for v in self.devices.values() if v.get("last_seen", 0) > time.time() - settings.SCREEN_TTL) + + def status(self): + with self._lock: + self._prune() + devices = [ + { + "device_id": cid, + "role": v.get("role", ""), + "parent_id": v.get("parent_id", ""), + } + for cid, v in self.devices.items() + ] + return { + "mqtt_connected": self.connected, + "mqtt_host": f"{settings.MQTT_HOST}:{settings.MQTT_PORT}", + "screens_online": len(devices), + "devices": devices, + "last_command": self.last_command, + } + + # ---------- 发布 ---------- + def publish(self, topic, payload, qos=1, retain=False): + if not self.client or not self.connected: + log.debug("MQTT 未连接,丢弃发布 %s", topic) + return False + try: + info = self.client.publish(topic, json.dumps(payload, ensure_ascii=False), qos=qos, retain=retain) + return info.rc == mqtt.MQTT_ERR_SUCCESS + except Exception as e: # noqa: BLE001 + log.warning("MQTT 发布失败: %s", e) + return False + + def register_device(self, device_id, role="", parent_id=""): + """前端上报设备:唯一 device_id + 角色(main/secondary) + 从属主屏 parent_id。""" + device_id = (device_id or "").strip() + if not device_id: + return + with self._lock: + cur = self.devices.get(device_id, {}) + if role: + cur["role"] = role + if parent_id: + cur["parent_id"] = parent_id + cur["last_seen"] = time.time() + self.devices[device_id] = cur + self._prune() + log.info("mqtt: 设备注册 id=%s role=%s parent=%s", device_id, role or cur.get("role"), parent_id or cur.get("parent_id")) + + def publish_command(self, action, params=None, screen_id=None, screen_role=None): + """按客户端(设备)路由下发指令: + · screen_id 精确到某台设备 → 只发到该设备独有 topic + · screen_role(main/secondary) → 解析到该角色的设备,发到其独有 topic + · 两者皆空(全部)→ 遍历在线设备列表,依次下发到各自独有 topic + 客户端只订阅自己的独有频道,因此定向指令绝不会被其它屏幕收到。""" + payload = { + "cmd_id": new_cmd_id(), + "ts": int(time.time() * 1000), + "action": action, + "params": params or {}, + } + target = None + requested = bool(screen_id or screen_role in ("main", "secondary")) + if screen_id: + target = (screen_id or "").strip() + elif screen_role in ("main", "secondary"): + with self._lock: + for cid, v in self.devices.items(): + if v.get("role") == screen_role: + target = cid + break + if target: + # 定向:只发到目标设备独有频道 + return self._publish_to(payload, target, screen_role) + if requested: + # 指定了目标但未找到对应设备:不广播,避免误发到全部 + log.warning("mqtt: 目标设备未找到(id=%s role=%s),未下发", screen_id, screen_role) + payload["published"] = False + with self._lock: + self.last_command = {"action": action, "params": params or {}, "ts": payload["ts"], "published": False, "topic": "none"} + return payload + + # 全部:遍历在线设备列表,依次下发到各自独有频道 + with self._lock: + cutoff = time.time() - settings.SCREEN_TTL + online = [cid for cid, v in self.devices.items() if v.get("last_seen", 0) > cutoff] + ok = True + published_topics = [] + for cid in online: + p = dict(payload) + p["screen_id"] = cid + with self._lock: + p["screen_role"] = self.devices.get(cid, {}).get("role", "") + if self.publish(f"{settings.TOPIC_COMMAND}/{cid}", p): + bus.emit(p) + published_topics.append(cid) + else: + ok = False + payload["published"] = ok + payload["screen_id"] = "" + payload["screen_role"] = "both" + with self._lock: + self.last_command = { + "action": action, + "params": params or {}, + "ts": payload["ts"], + "published": ok, + "topic": "all", + } + log.info("mqtt: 全部下发 %s 到 %d 台设备", action, len(published_topics)) + return payload + + def _publish_to(self, payload, target, screen_role=""): + """发到指定设备独有频道,并记录 last_command。""" + topic = f"{settings.TOPIC_COMMAND}/{target}" + payload["screen_id"] = target + with self._lock: + payload["screen_role"] = self.devices.get(target, {}).get("role", screen_role or "") + ok = self.publish(topic, payload) + payload["published"] = ok + with self._lock: + self.last_command = { + "action": payload["action"], + "params": payload.get("params") or {}, + "ts": payload["ts"], + "published": ok, + "topic": topic, + } + if ok: + bus.emit(payload) + return payload + + def publish_tick(self, snapshot): + payload = {"ts": int(time.time() * 1000), "snapshot": snapshot} + self.publish(settings.TOPIC_TICK, payload, qos=0) + + +hub = MqttHub() diff --git a/backend/app/rag.py b/backend/app/rag.py new file mode 100644 index 0000000..15e45b1 --- /dev/null +++ b/backend/app/rag.py @@ -0,0 +1,460 @@ +# -*- coding: utf-8 -*- +"""园区知识库 —— 轻量本地 RAG(embedding + numpy 余弦检索,双路索引) + +升级为「细粒度分块 + LLM 生成问答对 + 双路向量索引(原句 + 问题)+ 余弦检索返回原文」: +- 知识源:backend/knowledge/park.md(按 H2 节 → 原子事实两级分块) +- 向量化:dashscope text-embedding-v4(云端 API,索引构建一次后缓存,构建时 L2 归一化) +- 问答:复用 llm._chat_once 对每块生成 3-5 个口语化问句,缓存到 kb_qa.json(无 key 自动单路降级) +- 检索:query 同时比对原句向量与问题向量,按块去重,返回 top_k 个原文块 +- 提供: + - retrieve(query, top_k) → 检索命中的原文块(对话动态注入) + - full_kb() → 整库原文(小库全量注入,供语音 instructions) + - brief() → 前 N 字摘要(兼容 /api/kb/brief 与检索无命中回退) +""" +import hashlib +import json +import logging +import os +import re +from pathlib import Path + +import numpy as np + +log = logging.getLogger("dpm.rag") + + +def _trunc(s, n=300): + """日志用截断:超长内容只留前 n 字并标注总长。""" + s = str(s) + return s if len(s) <= n else s[:n] + f"...(共{len(s)}字)" + + +KB_DIR = Path(__file__).resolve().parent.parent / "knowledge" +KB_GLOB = "*.md" # 知识库 = knowledge/ 下全部 markdown(park.md + opc.md 等) +META_FILE = KB_DIR / "kb_meta.json" # 元数据(JSON,可读):model/hash/chunks/questions/embed_texts/embed_owner +VECTOR_FILE = KB_DIR / "kb_vectors.npy" # 向量矩阵(二进制 numpy float32,快载小体积) +LEGACY_INDEX_FILE = KB_DIR / "kb_index.json" # 旧版单文件索引(迁移后删除) +QA_FILE = KB_DIR / "kb_qa.json" +EMBED_MODEL = "text-embedding-v4" +EMBED_BATCH = 10 # DashScope embedding 单批上限 +QA_BATCH = 5 # 每批几块生成问答 +CHUNK_MAX = 240 # 单块超过此长度按句拆 + +_index = None # 缓存 {chunks, questions, embed_texts, embed_owner, embeddings(归一化 np.ndarray)} + + +def _kb_text() -> str: + """拼接 knowledge/ 下全部 markdown 内容(按文件名排序,稳定顺序)。""" + parts = [] + for f in sorted(KB_DIR.glob(KB_GLOB)): + try: + parts.append(f.read_text("utf-8")) + except OSError as e: # noqa: BLE001 + log.warning("rag: 跳过知识文件 %s: %s", f.name, e) + return "\n\n".join(parts) + + +def _data_hash() -> str: + """全部知识文件内容签名,用于索引/问答缓存失效判断。""" + return hashlib.sha1(_kb_text().encode("utf-8")).hexdigest() + + +def _split_long(text: str, limit: int = CHUNK_MAX) -> list[str]: + """超长块按句号/分号/顿号等边界拆成多块,保留语义完整。""" + if len(text) <= limit: + return [text] + parts: list[str] = [] + cur = "" + for seg in re.split(r"(?<=[。!?;、])", text): + if len(cur) + len(seg) > limit and cur: + parts.append(cur.strip()) + cur = "" + cur += seg + if cur.strip(): + parts.append(cur.strip()) + return [p for p in parts if len(p) > 8] + + +def _chunk_md(text: str) -> list[str]: + """两级切分:H2 节标题作上下文前缀 + 节内原子事实。 + + 企业条目(加粗公司名行 `**...|公司名**` + 负责人/简介列表)合并为单个块, + 保证「企业名称 + 企业介绍」作为一个整体可被按名召回。 + """ + chunks: list[str] = [] + section = "" + para: list[str] = [] + company: list[str] | None = None # 正在累积的单个企业块 + + def flush_para(): + nonlocal para + if para: + t = "\n".join(para).strip() + if len(t) > 8: + chunks.extend(_split_long(t)) + para = [] + + def flush_company(): + nonlocal company + if company: + t = "\n".join(company).strip() + if len(t) > 8: + chunks.append(t) # 企业块不拆,保证名称+介绍同块 + company = None + + def add_fact(content: str): + full = f"{section}\n{content}" if section else content + if len(full) > 8: + chunks.extend(_split_long(full)) + + for line in text.splitlines(): + s = line.rstrip() + if s.startswith("# "): # H1:不产块 + flush_para(); flush_company() + continue + if s.startswith("## "): # H2 小节:切换上下文前缀 + flush_para(); flush_company() + section = s[3:].strip() + continue + if s.startswith("###"): # H3 子节:上下文(如分区),flush 企业 + flush_para(); flush_company() + para.append(s.strip()) + continue + stripped = s.strip() + if not stripped: + continue + # 企业条目:加粗公司名行作为单个企业块的起点 + if stripped.startswith("**") and "|" in stripped: + flush_para(); flush_company() + company = [stripped] + continue + if company is not None: + # 企业块的跟随行(负责人/简介等列表项) + if stripped.startswith("- ") or stripped.startswith("问:") or re.match(r"^\d+[\.、]\s", stripped): + company.append(stripped) + else: + flush_company() + para.append(stripped) + continue + if stripped.startswith("- "): # 列表项 = 原子事实 + flush_para(); add_fact(stripped) + elif re.match(r"^\d+[\.、]\s", stripped): # 编号步 + flush_para(); add_fact(stripped) + elif stripped.startswith("问:"): # 单条问答 + flush_para(); add_fact(stripped) + else: # 普通段落:累积后整段成块(超长再拆) + para.append(stripped) + flush_para() + flush_company() + return chunks + + +def _company_name(chunk: str) -> str: + """从企业块提取企业名称:`**加速1|云南派音人工智能科技有限公司**` → `云南派音人工智能科技有限公司`。""" + m = re.match(r"^\*\*[^|\n]*|([^*]+)\*\*", chunk) or re.match(r"^\*\*([^*]+)\*\*", chunk) + return m.group(1).strip() if m else "" + + +def _embed(texts: list[str]) -> list[list[float]]: + """dashscope text-embedding-v4 批量向量化(每批 ≤10 条,按 text_index 排序拼接)。""" + import dashscope + + api_key = os.environ.get("DASHSCOPE_API_KEY") or "" + out: list[list[float]] = [] + for i in range(0, len(texts), EMBED_BATCH): + batch = texts[i:i + EMBED_BATCH] + resp = dashscope.TextEmbedding.call( + model=EMBED_MODEL, + input=batch, + api_key=api_key, + ) + if resp.status_code != 200: + raise RuntimeError( + f"embedding API {resp.status_code}: {resp.code} {resp.message}" + ) + embs = sorted( + resp.output["embeddings"], + key=lambda e: e.get("text_index", e.get("index", 0)), + ) + out.extend(e["embedding"] for e in embs) + log.info("rag: embedding 完成 %d 条(%s,每批 %d)", len(texts), EMBED_MODEL, EMBED_BATCH) + return out + + +def _normalize(vectors: list[list[float]]) -> np.ndarray: + """转为 float32 np.ndarray 并做 L2 行归一化(点积即余弦相似度)。""" + arr = np.asarray(vectors, dtype=np.float32) + norms = np.linalg.norm(arr, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + return arr / norms + + +# ── LLM 问答生成 ──────────────────────────────────────────────── + +def _parse_json_array(text: str) -> list: + """解析模型返回的 JSON 数组:剥 markdown 围栏 → json.loads → 截取首尾方括号兜底。""" + text = (text or "").strip() + if text.startswith("```"): + lines = [l for l in text.splitlines() if not l.strip().startswith("```")] + text = "\n".join(lines).strip() + for candidate in (text,): + try: + return json.loads(candidate) + except Exception: + pass + i = text.find("[") + j = text.rfind("]") + if i != -1 and j > i: + try: + return json.loads(text[i:j + 1]) + except Exception: + pass + return [] + + +def _qa_for_batch(chunks: list[str]) -> list[list[str]]: + """对一批分块,用 LLM 生成每块的 3-5 个口语化问句(输出 JSON 数组,与块一一对应)。""" + from .llm import _chat_once + + numbered = "\n\n".join(f"[{i}]\n{c}" for i, c in enumerate(chunks)) + + def build_prompt(): + return ( + "下面是园区知识库的若干分块,每块标有 [编号]。" + "请为每一块生成 3-5 个用户可能会问的自然口语化中文问句," + "覆盖该块全部信息点与常见同义问法。" + f"严格只输出一个 JSON 数组,数组长度必须为 {len(chunks)}," + "第 i 个元素是第 i 块的问题字符串数组。不要输出任何其他文字或解释。\n\n" + + numbered + ) + + for attempt in range(2): + try: + data = _chat_once([{"role": "user", "content": build_prompt()}], with_tools=False) + content = data["choices"][0]["message"].get("content") or "" + arr = _parse_json_array(content) + if isinstance(arr, list) and arr: + out: list[list[str]] = [] + for i in range(len(chunks)): + qs = arr[i] if i < len(arr) and isinstance(arr[i], list) else [] + out.append([str(q).strip() for q in qs if str(q).strip()][:5]) + return out + except Exception as e: # noqa: BLE001 + log.warning("rag: QA 批生成失败(尝试 %d/%d): %s", attempt + 1, 2, e) + return [[] for _ in chunks] + + +def _first_line(chunk: str) -> str: + """取块首行(去标题),用作无问句时的兜底检索问题。""" + for ln in chunk.splitlines(): + ln = ln.strip() + if ln and not ln.startswith("#"): + return ln + return chunk[:60] + + +def _ensure_questions(chunks: list[str], questions) -> list[list[str]]: + """确保每块都有至少一个检索问题:未生成问句的块,用块首句兜底(保证全量覆盖)。""" + qs: list[list[str]] = [list(q or []) for q in (questions or [])] + if len(qs) < len(chunks): + qs.extend([] for _ in range(len(chunks) - len(qs))) + for i, c in enumerate(chunks): + if not qs[i]: + qs[i] = [_first_line(c)] + return qs + + +def _load_questions(chunks: list[str]) -> list[list[str]]: + """加载/生成问答缓存;返回与 chunks 对齐的问题数组(保证每块 ≥1 个问题)。无 key 时单路回退。""" + from .config import settings + + if not settings.DASHSCOPE_API_KEY: + log.warning("rag: 无 DASHSCOPE_API_KEY,跳过问答生成(单路检索)") + return _ensure_questions(chunks, [[] for _ in chunks]) + h = _data_hash() + if QA_FILE.exists(): + try: + data = json.loads(QA_FILE.read_text("utf-8")) + if data.get("data_hash") == h and data.get("chunks") == chunks: + log.info("rag: 问答缓存已加载(%d 块)", len(chunks)) + return _ensure_questions(chunks, data["questions"]) + except Exception: # noqa: BLE001 + log.warning("rag: 问答缓存损坏,重建") + questions: list[list[str]] = [] + total = len(chunks) + for i in range(0, total, QA_BATCH): + questions.extend(_qa_for_batch(chunks[i:i + QA_BATCH])) + log.info("rag: 问答生成进度 %d/%d", min(i + QA_BATCH, total), total) + questions = _ensure_questions(chunks, questions) + QA_FILE.write_text( + json.dumps({"data_hash": h, "chunks": chunks, "questions": questions}, ensure_ascii=False), + "utf-8", + ) + log.info("rag: 问答缓存已生成(%d 块)-> %s", len(chunks), QA_FILE) + return questions + + +# ── 双路向量索引 ──────────────────────────────────────────────── + +def _migrate_legacy(): + """把旧版单文件 kb_index.json(向量内嵌在 JSON)就地转为 kb_vectors.npy + kb_meta.json,再删除旧文件。""" + if not LEGACY_INDEX_FILE.exists() or META_FILE.exists(): + return + try: + d = json.loads(LEGACY_INDEX_FILE.read_text("utf-8")) + np.save(VECTOR_FILE, np.asarray(d["embeddings"], dtype=np.float32)) + META_FILE.write_text( + json.dumps({ + "model": d.get("model"), + "data_hash": d.get("data_hash"), + "chunks": d["chunks"], + "questions": d.get("questions", []), + "embed_texts": d["embed_texts"], + "embed_owner": d["embed_owner"], + }, ensure_ascii=False), + "utf-8", + ) + LEGACY_INDEX_FILE.unlink() + log.info("rag: 已迁移旧 kb_index.json -> kb_vectors.npy + kb_meta.json(无重建,未调 API)") + except Exception as e: # noqa: BLE001 + log.warning("rag: 旧索引迁移失败(将重建): %s", e) + + +def _load_index(): + """构建/加载双路向量索引(原句 + 生成问题)。向量存二进制 .npy,元数据存 JSON。""" + global _index + if _index is not None: + return _index + _migrate_legacy() + h = _data_hash() + if META_FILE.exists() and VECTOR_FILE.exists(): + try: + data = json.loads(META_FILE.read_text("utf-8")) + valid = ( + data.get("model") == EMBED_MODEL + and data.get("data_hash") == h + and len(data.get("embed_texts", [])) == len(data.get("embed_owner", [])) + ) + if valid: + vectors = np.load(VECTOR_FILE) # 已归一化 float32 + if len(vectors) == len(data["embed_texts"]): + _index = { + "chunks": data["chunks"], + "questions": data.get("questions", []), + "embed_texts": data["embed_texts"], + "embed_owner": data["embed_owner"], + "embeddings": vectors, + } + log.info("rag: 双路知识索引已加载(%d 块 → %d 向量, %s)", + len(_index["chunks"]), len(_index["embed_texts"]), EMBED_MODEL) + return _index + log.warning("rag: 索引失效(model/hash/长度不符),重建") + except Exception: # noqa: BLE001 + log.warning("rag: 索引损坏,重建") + # 构建 + chunks = _chunk_md(_kb_text()) + questions = _load_questions(chunks) + embed_texts: list[str] = [] + embed_owner: list[int] = [] + for i, c in enumerate(chunks): + embed_texts.append(c) + embed_owner.append(i) + # 企业块:把「企业名称」作为确定性问题加入索引,保证按名称可召回(名称为问、介绍为答) + cname = _company_name(c) + if cname: + embed_texts.append(cname) + embed_owner.append(i) + for q in questions[i]: + embed_texts.append(q) + embed_owner.append(i) + log.info("rag: 构建双路索引(%d 块 → %d 向量,%s embedding 中…)", + len(chunks), len(embed_texts), EMBED_MODEL) + arr = _normalize(_embed(embed_texts)) + _index = { + "chunks": chunks, + "questions": questions, + "embed_texts": embed_texts, + "embed_owner": embed_owner, + "embeddings": arr, + } + np.save(VECTOR_FILE, arr) + META_FILE.write_text( + json.dumps({ + "model": EMBED_MODEL, + "data_hash": h, + "chunks": chunks, + "questions": questions, + "embed_texts": embed_texts, + "embed_owner": embed_owner, + }, ensure_ascii=False), + "utf-8", + ) + log.info("rag: 双路索引构建完成(%d 块, dim=%d)-> %s + %s", + len(chunks), arr.shape[1], VECTOR_FILE, META_FILE) + return _index + + +# ── 对外接口 ──────────────────────────────────────────────────── + +def retrieve(query: str, top_k: int = 3, threshold: float = 0.3) -> list[str]: + """双路检索:query 同时比对原句与问题向量,按块去重,返回 top_k 个原文块。""" + if not query or not query.strip(): + return [] + idx = _load_index() + q = _normalize([_embed([query])[0]])[0] + sims = np.dot(idx["embeddings"], q) + order = np.argsort(-sims) + best: dict[int, float] = {} # owner -> 该块最高分 + for e in order: + owner = idx["embed_owner"][int(e)] + sc = float(sims[int(e)]) + if sc < threshold: + break + if owner not in best: + best[owner] = sc + if len(best) >= top_k: + break + ordered = sorted(best.items(), key=lambda kv: kv[1], reverse=True) + hits = [idx["chunks"][o] for o, _ in ordered] + log.info("rag: retrieve() query=%s top_k=%d threshold=%.2f -> 命中 %d 段", + _trunc(query, 120), top_k, threshold, len(hits)) + for j, (o, sc) in enumerate(ordered): + log.info("rag: [%d] %.4f %s", j, sc, _trunc(idx["chunks"][o], 300)) + return hits + + +def brief(max_chars: int = 1200) -> str: + """固定知识摘要(前 N 字;兼容 /api/kb/brief 与检索无命中回退)。""" + idx = _load_index() + out = [] + total = 0 + for c in idx["chunks"]: + if total + len(c) > max_chars: + break + out.append(c) + total += len(c) + text = "\n\n".join(out) + log.info("rag: brief() 知识库摘要 %d 字 -> %s", total, _trunc(text, 300)) + return text + + +def full_kb() -> str: + """整库原文(小库全量注入,用于语音 instructions,修复尾部知识丢失)。""" + idx = _load_index() + text = "\n\n".join(idx["chunks"]) + log.info("rag: full_kb() 全库 %d 字 / %d 块", len(text), len(idx["chunks"])) + return text + + +def build_instructions() -> str: + """语音助手的完整基础提示词 = 配置提示词 + 整库知识(服务端统一组装)。""" + from .config import settings + base = settings.S2S_INSTRUCTIONS.strip() + try: + kb = full_kb() + if kb: + base += f"\n\n【园区知识库】\n{kb}" + except Exception as e: # noqa: BLE001 + log.warning("rag: 知识库拼接失败(仅用基础提示词): %s", e) + log.info("rag: build_instructions() 语音系统提示词=%s", _trunc(base, 600)) + return base diff --git a/backend/app/routers.py b/backend/app/routers.py new file mode 100644 index 0000000..b65c963 --- /dev/null +++ b/backend/app/routers.py @@ -0,0 +1,660 @@ +# -*- coding: utf-8 -*- +"""全部 REST 路由 —— 数据 / 媒体 / 播放列表 / 设置 / AI / 展示控制""" + +import asyncio +import json +import logging +import time +from pathlib import Path + +from fastapi import APIRouter, Request, UploadFile, File +from fastapi.responses import JSONResponse, StreamingResponse +from pydantic import BaseModel, Field + +from .ai_tools import run_chat, run_tools +from .asr import transcribe as asr_transcribe +from .config import settings +from .event_bus import bus +from .llm import run_chat as llm_run_chat +from .mqtt import hub +from .sim_engine import sim_engine +from .storage import storage + +log = logging.getLogger("dpm.api") + +router = APIRouter() + +ALLOWED_EXT = {".mp4", ".mkv", ".avi", ".jpg", ".jpeg", ".png"} + + +def _media_type(path): + lower = path.lower() + if lower.endswith((".mp4", ".mkv", ".avi")): + return "video" + return "image" + + +# ==================== 数据模型 ==================== + +class SettingsBody(BaseModel): + volume: int | None = None + sfx_volume: int | None = None + play_mode: str | None = None + image_duration: int | None = None + fullscreen: bool | None = None + autostart: bool | None = None + username: str | None = None + password: str | None = None + + +class ActionBody(BaseModel): + action: str + + +class PathBody(BaseModel): + path: str + + +class UrlBody(BaseModel): + url: str + + +class StatePayload(BaseModel): + status: str = "" + index: int = 0 + name: str = "" + media_type: str = "" + + +class StateBody(BaseModel): + state: StatePayload + + +class DisplayCommandBody(BaseModel): + action: str + params: dict = Field(default_factory=dict) + screen_id: str = "" # 目标屏幕唯一 client_id(精确到某台) + screen_role: str = "" # main | secondary(按角色定位) + + +class ChatMessage(BaseModel): + role: str + content: str + + +class ChatBody(BaseModel): + messages: list[ChatMessage] + screen_id: str = "" # 发起对话的屏幕设备 id(工具结果只发到该屏) + + +# ==================== 认证 / 设置 ==================== + +@router.get("/api/health") +async def health(): + """健康检查(Docker healthcheck / 运维探活)""" + return { + "ok": True, + "service": "dpm-backend", + "mqtt": hub.connected if getattr(hub, "connected", None) is not None else False, + "ts": __import__("time").time(), + } + + +@router.post("/api/login") +async def login(request: Request): + form = await request.form() + s = storage.get_settings() + return JSONResponse({"success": form.get("username") == s["username"] and form.get("password") == s["password"]}) + + +@router.get("/api/settings") +async def get_settings(): + s = storage.get_settings() + return {k: s[k] for k in ("volume", "sfx_volume", "play_mode", "image_duration", "fullscreen", "autostart")} + + +@router.get("/api/config") +async def runtime_config(request: Request): + """运行配置(供前端启动引导覆盖):MQTT 地址/账号、API 基址、语音地址 + —— 打包部署时展播端从后端拉取,避免构建期写死的局域网 IP 失效 + """ + ws = settings.MQTT_WS_URL + # 后端可达主机:优先取客户端实际访问本服务的 Host(与 api_base 一致), + # 回退到从 MQTT broker 地址提取。 + host = "192.168.1.9" + try: + host = ws.split("://", 1)[1].split(":", 1)[0] + except Exception: # noqa: BLE001 + pass + try: + rh = (request.headers.get("host") or "").split(":")[0] + if rh: + host = rh + except Exception: # noqa: BLE001 + pass + # s2s 与后端同机启动 → voice_url 默认跟随后端主机;显式 DPM_VOICE_WS 优先 + voice_url = settings.S2S_WS_URL + if not settings.VOICE_WS and host: + voice_url = f"ws://{host}:{settings.S2S_PORT}/v1/realtime" + return { + "ok": True, + "mqtt_url": ws, + "mqtt_username": settings.MQTT_USERNAME or "", + "mqtt_password": settings.MQTT_PASSWORD or "", + "voice_url": voice_url, + "api_base": f"http://{host}:{settings.PORT}", + "broker_host": host, + } + + +@router.post("/api/settings") +async def update_settings(body: SettingsBody): + storage.update_settings(**body.model_dump(exclude_none=True)) + s = storage.get_settings() + hub.publish_command("settings_changed", { + "volume": s["volume"], "sfx_volume": s["sfx_volume"], "play_mode": s["play_mode"], + }) + return {"ok": True} + + +# ==================== 媒体 ==================== + +def _list_media(): + files = [] + media_dir = Path(settings.MEDIA_DIR) + if media_dir.exists(): + for p in sorted(media_dir.iterdir()): + if not p.is_file() or p.suffix.lower() not in ALLOWED_EXT: + continue + files.append({ + "name": p.name, "relative_path": p.name, + "type": _media_type(p.name), "url": f"/file/{p.name}", "source": "local", + }) + for item in storage.get_url_media(): + files.append({ + "name": item.get("name", item.get("url", "")), + "relative_path": item.get("url", ""), + "type": item.get("type", "image"), + "url": item.get("url", ""), + "source": "url", + }) + return {"files": files} + + +@router.get("/media") +async def list_media(): + return _list_media() + + +@router.post("/upload") +async def upload(file: UploadFile = File(...)): + name = file.filename or "file" + ext = Path(name).suffix.lower() + if ext not in ALLOWED_EXT: + return JSONResponse({"ok": False, "error": "不支持的文件类型"}, status_code=400) + settings.MEDIA_DIR.mkdir(parents=True, exist_ok=True) + stem = Path(name).stem + new_name = f"{stem}_{time.strftime('%Y%m%d%H%M%S')}{ext}" + save_path = settings.MEDIA_DIR / new_name + with save_path.open("wb") as f: + while True: + chunk = await file.read(1024 * 1024) + if not chunk: + break + f.write(chunk) + hub.publish_command("playlist_changed") + return {"ok": True, "name": new_name} + + +@router.post("/api/delete") +async def delete_media(body: PathBody): + path = body.path + if not path.startswith(("http://", "https://")): + target = settings.MEDIA_DIR / path + try: + if target.exists() and target.is_file(): + target.unlink() + except OSError: + pass + storage.delete_media(path) + hub.publish_command("playlist_changed") + return {"ok": True} + + +@router.post("/api/media/add-url") +async def add_url(body: UrlBody): + url = body.url.strip() + if not url: + return {"ok": False} + name = url.rsplit("/", 1)[-1].split("?", 1)[0] or url + t = _media_type(url) + is_new = storage.add_url_media(url, name, t) + return {"ok": True, "duplicate": None if is_new else True} + + +# ==================== 播放列表 ==================== + +def _playlist_files(): + files = [] + media_dir = Path(settings.MEDIA_DIR) + for item in storage.get_playlist(): + path = item.get("path", "") + if item.get("source") == "url": + files.append({ + "name": item.get("name") or path, + "relative_path": path, "type": _media_type(path), "url": path, "source": "url", + }) + else: + fp = media_dir / path + if fp.exists(): + files.append({ + "name": fp.name, "relative_path": path, + "type": _media_type(path), "url": f"/file/{path}", "source": "local", + }) + s = storage.get_settings() + return {"files": files, "volume": s["volume"], "play_mode": s["play_mode"], "image_duration": s["image_duration"]} + + +@router.get("/api/playlist") +async def get_playlist(): + return _playlist_files() + + +@router.post("/api/playlist/add") +async def add_playlist(body: PathBody): + storage.add_to_playlist(body.path) + hub.publish_command("playlist_changed") + return {"ok": True} + + +@router.post("/api/playlist/remove") +async def remove_playlist(body: PathBody): + storage.remove_from_playlist(body.path) + hub.publish_command("playlist_changed") + return {"ok": True} + + +@router.post("/api/playlist/play") +async def play_playlist_item(body: PathBody): + """指定媒体立即播放:加入播放列表(如未在)→ MQTT 广播 play_target → 大屏跳转播放""" + path = body.path + storage.add_to_playlist(path) + hub.publish_command("playlist_changed") + cmd = hub.publish_command("play_target", {"path": path}) + return {"ok": True, "cmd_id": cmd.get("cmd_id")} + + +# ==================== 播放控制 / 状态 ==================== + +_CONTROL_ACTIONS = {"play", "pause", "next", "prev"} + + +@router.post("/api/control") +async def control(body: ActionBody): + if body.action not in _CONTROL_ACTIONS: + return JSONResponse({"ok": False, "error": "action 必须是 play/pause/next/prev"}, status_code=400) + hub.publish_command(body.action) + return {"ok": True, "action": body.action} + + +@router.get("/api/state") +async def get_state(): + return {"state": {"status": "unknown", "index": 0, "name": "", "media_type": ""}} + + +@router.post("/api/state") +async def update_state(body: StateBody): + hub.publish_command("state_update", body.state.model_dump()) + return {"ok": True} + + +@router.post("/api/display-command") +async def display_command(body: ActionBody): + if body.action == "minimize": + hub.publish_command("minimize") + return {"ok": True, "action": "minimize"} + return JSONResponse({"ok": False, "error": "不支持的显示命令"}, status_code=400) + + +# ==================== 数据(大屏全部数据来自后端) ==================== + +@router.get("/api/dashboard/snapshot") +async def dashboard_snapshot(): + return sim_engine.snapshot() + + +@router.get("/api/dashboard/overview") +async def dashboard_overview(): + return sim_engine.snapshot() + + +# ==================== 园区企业 ==================== + +_COMPANIES = [ + {"name": "云南派音人工智能科技", "zone": "加速区", "room": "A1", "color": "#4c8dff"}, + {"name": "米勒克尔蓝宝石珠宝", "zone": "加速区", "room": "A2", "color": "#4c8dff"}, + {"name": "中泰研学合作", "zone": "加速区", "room": "A3", "color": "#4c8dff"}, + {"name": "云南宸中低空经济", "zone": "加速区", "room": "A4", "color": "#4c8dff"}, + {"name": "昆明智海银高文化科技", "zone": "加速区", "room": "A5", "color": "#4c8dff"}, + {"name": "AI机器人大模型训练", "zone": "加速区", "room": "A6", "color": "#4c8dff"}, + {"name": "云南廷秀文旅康养", "zone": "加速区", "room": "A7", "color": "#4c8dff"}, + {"name": "瀚颖AI+教育信息咨询", "zone": "加速区", "room": "A8", "color": "#4c8dff"}, + {"name": "云南大学AI+创业平台", "zone": "加速区", "room": "A9", "color": "#4c8dff"}, + {"name": "仰光客厅", "zone": "国际区", "room": "I1", "color": "#22d3ee"}, + {"name": "云南上古绝学文化", "zone": "国际区", "room": "I2", "color": "#22d3ee"}, + {"name": "中越生物医疗", "zone": "国际区", "room": "I3", "color": "#22d3ee"}, + {"name": "酷享野农AI农业", "zone": "国际区", "room": "I4", "color": "#22d3ee"}, + {"name": "滇缅国际设计", "zone": "国际区", "room": "I5", "color": "#22d3ee"}, + {"name": "昆明舒诺生物科技", "zone": "国际区", "room": "I6", "color": "#22d3ee"}, + {"name": "达岸教育管理", "zone": "国际区", "room": "I7", "color": "#22d3ee"}, + {"name": "Facebook越南跨境电商", "zone": "成长区", "room": "G1", "color": "#34d399"}, + {"name": "研X同行者网络", "zone": "成长区", "room": "G2", "color": "#34d399"}, + {"name": "朵哈·玫瑰特色产业链", "zone": "成长区", "room": "G3", "color": "#34d399"}, + {"name": "昆明云韵体育", "zone": "成长区", "room": "G4", "color": "#34d399"}, + {"name": "南菌优培食用菌", "zone": "成长区", "room": "G5", "color": "#34d399"}, + {"name": "五华区丽裳文化", "zone": "成长区", "room": "G6", "color": "#34d399"}, + {"name": "云南星瑞航空", "zone": "成长区", "room": "G7", "color": "#34d399"}, + {"name": "综合直播私域平台", "zone": "成长区", "room": "G8", "color": "#34d399"}, + {"name": "园区管理办公室", "zone": "园区管理", "room": "M", "color": "#fbbf24"}, +] + + +@router.get("/api/park/companies") +async def park_companies(): + return {"companies": _COMPANIES} + + +@router.get("/api/park/zones") +async def park_zones(): + zones = ["加速区", "国际区", "成长区"] + counts = {z: sum(1 for c in _COMPANIES if c["zone"] == z) for z in zones} + colors = {"加速区": "#4c8dff", "国际区": "#22d3ee", "成长区": "#34d399"} + return {"zones": [{"name": z, "count": counts[z], "color": colors[z]} for z in zones]} + + +# ==================== AI 对话(通义千问 + 工具调用) ==================== + +# 预设问题(围绕昆明市大学生创业园 / OPC 园区性质设计,前端从后端读取) +AI_GROUPED_QUESTIONS = [ + { + "title": "入驻与流程", + "questions": [ + "如何申请入驻园区?", + "园区入驻的条件有哪些?", + "入驻需要准备哪些材料?", + "入驻流程是怎样的?", + "入驻评审如何打分?", + "入驻需要多长时间?", + ], + }, + { + "title": "政策与扶持", + "questions": [ + "园区有哪些创业政策扶持?", + "如何申请创业补贴?", + "如何申请创业担保贷款?", + "对高校毕业生有什么优惠?", + "科技成果转化有哪些支持?", + ], + }, + { + "title": "OPC 概念", + "questions": [ + "什么是 OPC?", + "OPC 创业有哪些模式?", + "OPC 适合哪些人?", + "OPC 创业者从哪里开始?", + "OPC 常用的人工智能工具有哪些?", + ], + }, + { + "title": "场地与服务", + "questions": [ + "园区提供哪些免费办公空间?", + "园区有哪些孵化服务?", + "园区提供哪些创业辅导?", + "园区有哪些基础配套?", + "园区可以免费使用哪些资源?", + ], + }, + { + "title": "园区企业介绍", + "questions": [ + "介绍一下园区入驻企业", + "园区有哪些 AI 科技企业?", + "园区有哪些跨境电商企业?", + "园区有哪些生物医药企业?", + "介绍一下云南派音人工智能科技", + "介绍一下米勒克尔蓝宝石珠宝", + "介绍一下研X同行者网络", + ], + }, +] + +AI_OPC_TOOLS = [ + "DeepSeek", "通义千问", "ChatGPT", "豆包", + "Midjourney", "Stable Diffusion", "剪映", "Notion AI", + "WPS AI", "GitHub Copilot", +] + + +@router.get("/api/ai/questions") +async def ai_questions(): + """预设问题列表(围绕园区性质设计,前端右侧面板展示)""" + return { + "groups": AI_GROUPED_QUESTIONS, + "opcTools": AI_OPC_TOOLS, + } + + +@router.post("/api/ai/chat") +async def ai_chat(body: ChatBody): + """大模型对话:工具调用(切页/控制/卡片/通知)经 MQTT 广播到所有大屏 + 返回 {reply, tools, model};tools 供请求端本地同步执行""" + messages = [m.model_dump() for m in body.messages] + result = llm_run_chat(messages, body.screen_id or "") + return result + + +@router.post("/api/ai/asr") +async def ai_asr(file: UploadFile = File(...), format: str = "m4a"): + """语音识别:上传录音 → 阿里云 paraformer 转写为文本""" + data = await file.read() + if not data: + return JSONResponse({"ok": False, "error": "空音频"}, status_code=400) + try: + text = asr_transcribe(data, fmt=format) + return {"ok": True, "text": text} + except Exception as e: # noqa: BLE001 + log.warning("ASR 转写失败: %s", e) + return JSONResponse({"ok": False, "error": str(e)}, status_code=502) + + +# ==================== 展示控制(管理端 → MQTT) ==================== +# 全部 MQTT 前端控制命令白名单(与 docs/mqtt-commands.md 保持一致) +_VALID_DISPLAY_ACTIONS = { + # 页面 + "navigate", "navigate_rel", + # 全局视觉 + "vision_set", + # AI 助手页 + "ai_input", "ai_preset", "ai_company", "ai_zone", + # 语音对话页 + "voice_start", "voice_stop", "voice_refresh", + # 媒体 + "play", "pause", "next", "prev", "set_mode", "play_target", + # 双屏 + "dual_screen", + # 全局 + "alert", "show_card", "minimize", "settings_changed", "playlist_changed", +} + + +class RegisterBody(BaseModel): + device_id: str = "" # 唯一设备标识(MQTT client_id) + role: str = "" # main | secondary + parent_id: str = "" # 副屏从属的主屏 device_id;主屏为空 + + +@router.post("/api/display/register") +async def display_register(body: RegisterBody): + """设备注册:前端上报唯一 device_id + 角色 + 从属主屏,后端维护设备表供精准控制。""" + hub.register_device(body.device_id, body.role, body.parent_id) + return {"ok": True, "device_id": body.device_id, "role": body.role, "parent_id": body.parent_id} + + +@router.post("/api/display/command") +async def display_command_publish(body: DisplayCommandBody): + """管理端/任意客户端通过 REST 发控制指令 → 后端转 MQTT 广播给所有大屏""" + if body.action not in _VALID_DISPLAY_ACTIONS: + return JSONResponse({"ok": False, "error": f"不支持的指令: {body.action}"}, status_code=400) + # 按屏幕终端路由:screen_id 精确到某客户端;screen_role 按主/副定位;皆空 → 全局广播 + cmd = hub.publish_command(body.action, body.params, body.screen_id or None, body.screen_role or None) + return {"ok": cmd.get("published", False), "cmd_id": cmd["cmd_id"], "action": body.action, + "screen_id": body.screen_id or "", "screen_role": body.screen_role or "both", + "mqtt_connected": hub.connected} + + +@router.get("/api/display/state") +async def display_state(): + return hub.status() + + +# ==================== SSE 兼容通道(MQTT 不可用时前端回退) ==================== + +@router.get("/api/events") +async def sse_events(request: Request): + q = bus.subscribe() + + async def gen(): + try: + while True: + if await request.is_disconnected(): + break + try: + data = await asyncio.wait_for(q.get(), timeout=15) + yield f"data: {data}\n\n" + except asyncio.TimeoutError: + yield ": keepalive\n\n" + finally: + bus.unsubscribe(q) + + return StreamingResponse(gen(), media_type="text/event-stream") + + +# ==================== 视觉识别事件上报(前端摄像头实时识别 → 后端日志/MQTT) ==================== +class VisionEventBody(BaseModel): + event: str # camera_on|camera_off|detecting|facing|triggered|silent|error|model_error + faces: int = 0 + dwell_ms: int = 0 + detail: str = "" + +@router.post("/api/vision/event") +async def vision_event(body: VisionEventBody): + """前端摄像头识别状态/触发上报;后端记录详细日志。 + 主动问候已关闭,故不再对 triggered 广播 alert(避免右上角弹「已主动问候」误导提示)。""" + log.info( + "vision event=%s faces=%d dwell_ms=%d detail=%s", + body.event, body.faces, body.dwell_ms, body.detail, + ) + return {"ok": True} + + +# ==================== YOLO 人脸检测(前端抽帧 → 后端推理) ==================== +class VisionFrameBody(BaseModel): + image: str = "" # JPEG base64(不含 data: 前缀) + conf: float = 0.0 # 可选:覆盖置信度阈值 + +@router.post("/api/vision/frame") +def vision_frame(body: VisionFrameBody): + """接收前端抽帧 JPEG base64,后端 YOLO 推理返回人脸框(faces/boxes/latency_ms)。 + + 普通 def 由 FastAPI 线程池执行(推理约 100-200ms),不阻塞事件循环。 + """ + img_b64 = (body.image or "").strip() + if not img_b64: + log.warning("vision frame: 缺少 image") + return {"ok": False, "error": "missing image"} + from .vision_yolo import predict_base64 + return predict_base64(img_b64) + + +class VisionLlmBody(BaseModel): + image: str = "" # JPEG base64 + prompt: str = "" # 可选,自定义识别指令 + +@router.post("/api/vision/llm") +def vision_llm(body: VisionLlmBody): + """对话开场画面识别:多模态 LLM(qwen3-vl-flash)识别人数/性别,返回 {ok, people, males, females, desc}。""" + img = (body.image or "").strip() + if not img: + return {"ok": False, "error": "missing image"} + try: + from .vision_llm import analyze_scene + result = analyze_scene(img, body.prompt) + log.info("api: /api/vision/llm -> people=%s males=%s females=%s desc=%s", + result.get("people"), result.get("males"), result.get("females"), + (result.get("desc") or "")[:60]) + return result + except Exception as e: # noqa: BLE001 + log.warning("vision llm 失败: %s", e) + return {"ok": False, "error": str(e)} + + +# ==================== 智能体工具调用 + 知识库 ==================== +class ToolsExecBody(BaseModel): + name: str = "" + args: dict = {} + screen_id: str = "" # 发起语音对话的屏幕设备 id(工具结果只发到该屏) + +@router.post("/api/tools/exec") +def tools_exec(body: ToolsExecBody): + """执行智能体工具(LLM function calling):name + args → 结果字符串""" + name = (body.name or "").strip() + if not name: + return {"ok": False, "error": "missing tool name"} + from .tools import exec_tool + result = exec_tool(name, body.args or {}, body.screen_id or "") + log.info("api: /api/tools/exec name=%s args=%s screen=%s result=%s", name, + json.dumps(body.args or {}, ensure_ascii=False)[:300], + body.screen_id or "all", str(result)[:400]) + return {"ok": True, "result": result} + +@router.get("/api/kb/brief") +def kb_brief(max_chars: int = 1200): + """园区知识摘要(前端拼进对话 instructions)""" + try: + from .rag import brief + result = brief(max_chars=max_chars) + log.info("api: /api/kb/brief max_chars=%d -> %s", max_chars, str(result)[:300]) + return {"ok": True, "brief": result} + except Exception as e: # noqa: BLE001 + log.warning("kb brief 失败: %s", e) + return {"ok": False, "error": str(e)} + +@router.get("/api/kb/retrieve") +def kb_retrieve(q: str = "", top_k: int = 3): + """知识库检索(query → 相关段落)""" + if not q.strip(): + return {"ok": False, "error": "missing q"} + try: + from .rag import retrieve + chunks = retrieve(q, top_k=top_k) + log.info("api: /api/kb/retrieve q=%s top_k=%d -> %d 段", q[:100], top_k, len(chunks)) + return {"ok": True, "chunks": chunks} + except Exception as e: # noqa: BLE001 + log.warning("kb retrieve 失败: %s", e) + return {"ok": False, "error": str(e)} + +@router.get("/api/s2s/instructions") +def s2s_instructions(): + """语音助手基础提示词(服务端统一组装:配置提示词 + 园区知识库)""" + try: + from .rag import build_instructions + instructions = build_instructions() + log.info("api: /api/s2s/instructions -> 系统提示词(%d 字)=%s", + len(instructions), instructions[:600]) + return {"ok": True, "instructions": instructions} + except Exception as e: # noqa: BLE001 + log.warning("s2s instructions 构建失败: %s", e) + return {"ok": False, "error": str(e)} diff --git a/backend/app/s2s_bridge.py b/backend/app/s2s_bridge.py new file mode 100644 index 0000000..4cf01d4 --- /dev/null +++ b/backend/app/s2s_bridge.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +"""s2s 实时语音栈(VAD 本地 + 云 ASR/LLM/TTS)桥接 + +在 DPM 进程内以后台线程运行 s2s 的 RealtimeServer(默认 :8765 /v1/realtime)。 +- 复用 live-avatar 云化栈(speech-to-speech v0.2.11 + cloud-migration 补丁)。 +- 依赖已并入 backend/pyproject.toml;本模块负责按 DPM Settings 组装并启动。 +- 前端语音对话页通过 WS 直连 ws://:/v1/realtime。 +""" + +import logging +import threading +import time + +from .config import settings + +log = logging.getLogger("dpm.s2s") + + +def _build_pool(stop_event: threading.Event): + """按 DPM 配置构建 s2s 管线池(含 VAD/silero 加载 + LLM warmup,耗时约 30~60s)。""" + import sys + + from speech_to_speech.api.openai_realtime.websocket_router import create_app + from speech_to_speech.s2s_pipeline import ( + _build_realtime_pipeline_unit, + parse_arguments, + prepare_all_args, + ) + + # 与 start_backend.sh 等价的参数(api_key 留空 → handler 从 env DASHSCOPE_API_KEY 读取) + sys.argv = [ + "dpm-s2s", + "--mode", "realtime", + "--stt", "dashscope-asr", + "--llm_backend", "chat-completions", + "--tts", "qwen3-cloud", + "--model_name", settings.S2S_LLM_MODEL, + "--responses_api_base_url", settings.LLM_BASE_URL, + "--responses_api_api_key", settings.DASHSCOPE_API_KEY, + "--dashscope_asr_model", settings.S2S_STT_MODEL, + "--qwen3_cloud_model", settings.S2S_TTS_MODEL, + "--qwen3_cloud_voice", settings.S2S_TTS_VOICE, + "--enable_live_transcription", "false", + "--thresh", "0.6", + "--min_silence_ms", "500", + "--min_speech_ms", "500", + "--log_level", settings.S2S_LOG_LEVEL, + ] + args = parse_arguments() + prepare_all_args( + args.module_kwargs, + args.whisper_stt_handler_kwargs, + args.paraformer_stt_handler_kwargs, + args.faster_whisper_stt_handler_kwargs, + args.mlx_audio_whisper_stt_handler_kwargs, + args.parakeet_tdt_stt_handler_kwargs, + args.dashscope_asr_stt_handler_kwargs, + args.language_model_handler_kwargs, + args.responses_api_language_model_handler_kwargs, + args.chat_tts_handler_kwargs, + args.facebook_mms_tts_handler_kwargs, + args.pocket_tts_handler_kwargs, + args.kokoro_tts_handler_kwargs, + args.qwen3_tts_handler_kwargs, + args.qwen3_cloud_tts_handler_kwargs, + ) + + pool = [ + _build_realtime_pipeline_unit( + index=i, + stop_event=stop_event, + module_kwargs=args.module_kwargs, + vad_handler_kwargs=args.vad_handler_kwargs, + whisper_stt_handler_kwargs=args.whisper_stt_handler_kwargs, + faster_whisper_stt_handler_kwargs=args.faster_whisper_stt_handler_kwargs, + paraformer_stt_handler_kwargs=args.paraformer_stt_handler_kwargs, + mlx_audio_whisper_stt_handler_kwargs=args.mlx_audio_whisper_stt_handler_kwargs, + parakeet_tdt_stt_handler_kwargs=args.parakeet_tdt_stt_handler_kwargs, + dashscope_asr_stt_handler_kwargs=args.dashscope_asr_stt_handler_kwargs, + language_model_handler_kwargs=args.language_model_handler_kwargs, + responses_api_language_model_handler_kwargs=args.responses_api_language_model_handler_kwargs, + chat_tts_handler_kwargs=args.chat_tts_handler_kwargs, + facebook_mms_tts_handler_kwargs=args.facebook_mms_tts_handler_kwargs, + pocket_tts_handler_kwargs=args.pocket_tts_handler_kwargs, + kokoro_tts_handler_kwargs=args.kokoro_tts_handler_kwargs, + qwen3_tts_handler_kwargs=args.qwen3_tts_handler_kwargs, + qwen3_cloud_tts_handler_kwargs=args.qwen3_cloud_tts_handler_kwargs, + ) + for i in range(settings.S2S_NUM_PIPELINES) + ] + + app = create_app(pool=pool, stop_event=stop_event) + return app, pool + + +def start_s2s_backend(stop_event: threading.Event) -> None: + """后台线程构建管线并启动 RealtimeServer(阻塞调用,放线程里跑)。""" + + def _run() -> None: + from speech_to_speech.api.openai_realtime.server import RealtimeServer + + # LLM warmup / 云 ASR/TTS 连接在弱网下会瞬断:整个池构建重试几次 + last_exc: Exception | None = None + log.info( + "s2s: 启动配置 STT=%s LLM=%s TTS=%s(%s) pipelines=%d ws://%s:%d/v1/realtime retries=%d", + settings.S2S_STT_MODEL, settings.S2S_LLM_MODEL, settings.S2S_TTS_MODEL, + settings.S2S_TTS_VOICE, settings.S2S_NUM_PIPELINES, + settings.S2S_HOST, settings.S2S_PORT, settings.S2S_BUILD_RETRIES, + ) + for attempt in range(settings.S2S_BUILD_RETRIES): + try: + log.info( + "s2s: 构建语音管线池(%d 路,尝试 %d/%d,首次约 30~60s)…", + settings.S2S_NUM_PIPELINES, + attempt + 1, + settings.S2S_BUILD_RETRIES, + ) + app, pool = _build_pool(stop_event) + server = RealtimeServer( + stop_event=stop_event, + pool=pool, + host=settings.S2S_HOST, + port=settings.S2S_PORT, + ) + # 关键:与 s2s_pipeline.main() 的 realtime 分支一致——用 ThreadManager + # 启动 RealtimeServer + 全部 handler 线程(VAD/STT/LLM/TTS)。 + # 只跑 server 不启动 handler 线程会导致管线不处理任何音频。 + from speech_to_speech.utils.thread_manager import ThreadManager + + all_handlers = [server] + [h for unit in pool for h in unit.handlers] + thread_manager = ThreadManager(all_handlers) + thread_manager.start() + log.info("s2s: 实时语音服务 ws://%s:%d/v1/realtime", settings.S2S_HOST, settings.S2S_PORT) + thread_manager.wait() + return + except Exception as e: # noqa: BLE001 + last_exc = e + log.warning("s2s: 构建尝试 %d/%d 失败:%s", attempt + 1, settings.S2S_BUILD_RETRIES, e) + if attempt + 1 < settings.S2S_BUILD_RETRIES: + time.sleep(settings.S2S_BUILD_RETRY_DELAY) + log.error("s2s: 启动失败(DPM_S2S_ENABLED=0 可关闭):%s", last_exc) + + threading.Thread(target=_run, daemon=True).start() diff --git a/backend/app/sim_engine.py b/backend/app/sim_engine.py new file mode 100644 index 0000000..d777048 --- /dev/null +++ b/backend/app/sim_engine.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +"""园区数据引擎(后端)—— 真实数据 + 测算演示 + 数据来源(用户指定口径): + · 《昆明市大学生创业园运行情况统计表(2026年7月).xls》(官方月报) + · 《昆明市创业园宣传册.pdf》 + 测算口径(用户确认): + · TOKEN:真实业务锚点月均 200 亿 tokens → 日均 ≈ 6.67 亿(200亿/30), + 实时速率 ≈ 7,700 t/s(日均/86400s),近30日累计 = 200 亿 + · 工具调用:按平均每次调用消耗约 2 万 tokens 折算(今日 ≈ 3.3 万次) + 其余统计指标为真实静态值;TOKEN/工具仅做轻微演示波动 +""" + +import random +import threading +import time + +# ---------- 真实锚点(仅来自 xls 统计表 + 附件1 省级材料 + 宣传册) ---------- +REAL = { + "as_of": "2026-07", + "token_monthly_yi": 200, # 月均消耗 200 亿 tokens(AI 平台业务锚点,非政府统计口径) + "token_daily_wan": 66666.7, # 日均 6.67 亿 tokens(200亿/30 ≈ 66666.7 万) + "token_rate": 7700, # 实时 t/s(6.67e8 / 86400 ≈ 7,716) + "tools_per_call_tokens": 150000, # 平均每次工具调用消耗 tokens(真实测算:12.5M输入 ÷ 81步 ≈ 15.4万) + "occupied": 38, # 实际入驻企业数(官方口径:《2026年7月运行情况统计表》38 家) + "capacity": 49, # 园区可容纳企业个数(统计表) + "invested": 560, # 开园以来累计投入运营资金(万元,统计表) + "jobs": 213, # 带动就业人数(统计表,当年累计口径) + "revenue_total": 252.15, # 生产经营总额(万元,统计表,当年累计口径) + "revenue_tax": 1.03, # 上缴税利总额(万元,统计表,当年累计口径) + "incubated_total": 181, # 累计孵化企业(附件1 省级绩效累计口径,截至2026年5月) + "in_incubation": 31, # 当前实有在孵创业实体(附件1) + "graduated": 149, # 累计成功孵化出园企业(统计表,开园以来累计) + "patents": 171, # 入驻团队发明专利(附件1 累计口径) + "jobs_total": 1529, # 累计带动就业(附件1 省级绩效累计口径) + "revenue_total_cum": 21911.14, # 入驻团队累计经营收入(万元,附件1 累计口径 ≈ 2.19 亿元) + "revenue_tax_cum": 426.2, # 入驻团队累计税收(万元,附件1 累计口径) + "mentors": 50, # 创业指导专家团队(附件1) + "success_rate": 95.91, # 近3年入孵实体孵化成功率(%)(附件1) + "area": 3000, # 园区建筑面积(㎡,统计表+宣传册) + "founded": 2009, # 成立年份(宣传册) + "province_level": 2012, # 获评省级创业园年份(宣传册) + "address": "昆明市民航路229号(人力资源中心1-2楼)", + "phone": "18087174891", + "email": "714066050@qq.com", +} + +# 入驻企业名录(《入驻企业信息表》39 家全称,与在园数统一口径) +COMPANIES = [ + "云南派音人工智能科技有限公司", "米勒克尔蓝宝石珠宝产业化(项目)", "中泰研学合作(项目)", + "云南宸中低空经济有限公司", "昆明智海银高文化科技有限公司", "人工智能机器人大模型训练(项目)", + "云南廷秀文旅康养服务(项目)", "瀚颖AI+教育信息咨询(项目)", "云南大学AI+联合创业服务平台(项目)", + "仰光客厅(项目)", "云南上古绝学文化发展有限公司", "千楣之路(项目)", + "中越生物医疗(项目)", "中越文旅交流(项目)", "绿态环保科技项目", + "联翼航空技术低空经济应用国际化拓展(项目)", "酷享野农AI农业(项目)", "滇缅国际设计(项目)", + "昆明舒诺生物科技有限责任公司", "达岸教育管理(云南)有限公司", "昆明滇油石化有限责任公司", + "花仙子园艺肥料(云南)有限公司", "Facebook越南市场普洱茶与建水紫陶壶跨境电商(项目)", + "研X——同行者网络(项目)", "鬼才明AI创意工作室(项目)", "朵哈·玫瑰特色产业链(项目)", + "昆明云韵体育有限责任公司", "昆明舒护安养老服务有限责任公司", + "南菌优培—食用菌专用生物制剂创新与高原特色野生菌高值化全链开发(项目)", + "五华区丽裳文化艺术工作室", "云南星瑞航空(项目)", "综合性云南直播私域服务平台", + "昆明屿澈电子商务有限公司", "中外青少年研学(项目)", "星杭商务信息咨询昆明有限责任公司", + "蓝智科技(项目)", "云品出滇·纸享万家(项目)", + "云迹轻旅—昆明本土一站式轻量化文旅服务工作室(项目)", "启元人工智能科技(项目)", +] + +# 宣传册四大孵化区域 +ZONES = [ + {"name": "OPC创业空间", "desc": "AI 技术支撑 · 数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域", "period": "拎包入驻 · 最长2年"}, + {"name": "创业苗圃区", "desc": "创意阶段 · 未注册企业团队 · 开放式共享工位", "period": "最长2年"}, + {"name": "孵化加速区", "desc": "已工商注册初创企业 · 独立办公空间", "period": "基础2年 · 可延1年(≤3年)"}, + {"name": "国际创客区", "desc": "归国留学生 · 外籍留学生 · 国际化配套服务", "period": "参照加速区"}, +] + +# 园区企业行业分布(按《入驻企业信息表》39 家企业简介分类统计) +INDUSTRY_MIX = [ + {"name": "人工智能", "value": 7}, + {"name": "跨境电商", "value": 5}, + {"name": "教育培训", "value": 4}, + {"name": "文旅康养", "value": 4}, + {"name": "文化创意", "value": 4}, + {"name": "生物医药", "value": 4}, + {"name": "低空经济", "value": 3}, + {"name": "现代农业", "value": 3}, + {"name": "绿色环保", "value": 2}, + {"name": "企业服务", "value": 2}, + {"name": "新材料", "value": 1}, +] + +# 园区实时动态(真实事件:园区官方公示 + 宣传册活动 + 互联网公开信息,最新在前) +FEED = [ + {"id": 24, "icon": "dna", "text": "园内企业「昆明舒诺生物科技」完成GEO服务体系建设(生成式引擎优化,聚焦宠物营养品牌数字化)", "time": "2026-08"}, + {"id": 1, "icon": "activity", "text": "2026年7月运行情况统计表填报完成:累计投入运营资金560万元、带动就业213人", "time": "2026-07-31"}, + {"id": 2, "icon": "radio", "text": "2026年第三期入驻创业企业(项目)评审结果在昆明市人社局官网公示", "time": "2026-07-02"}, + {"id": 3, "icon": "graduation", "text": "云南师范大学就业创业实践基地落地园区 · 云南旅游职业学院开展就业创业交流", "time": "2026-06-03"}, + {"id": 23, "icon": "radio", "text": "「PineSound」完成 ICP 备案(滇ICP备2026008517号)与公安备案,注册地址昆明市民航路301-307号", "time": "2026"}, + {"id": 4, "icon": "rocket", "text": "\"滇水逐梦·雨林创航\"昆明·西双版纳创业路演PK赛成功举办", "time": "2026-05-20"}, + {"id": 5, "icon": "flag", "text": "昆明市青年企业家协会创新创业基地在园区揭牌", "time": "2026-05-13"}, + {"id": 6, "icon": "users", "text": "\"聚力赋能·共创未来\"高校创新创业交流暨项目路演活动圆满举行", "time": "2026-05-08"}, + {"id": 7, "icon": "trophy", "text": "\"创赢未来\"2026创业大赛昆明选拔赛暨马兰花创业培训讲师大赛举行(13个项目参赛)", "time": "2026-04-22"}, + {"id": 8, "icon": "broadcast", "text": "2026年第二期创业企业(项目)入驻招募公告发布(官网 www.kmyc.gov.cn)", "time": "2026-04-15"}, + {"id": 21, "icon": "sparkles", "text": "「PineSound」AI驱动音频管理平台上线 pinesound.cn:智能配乐与音效生成,集成100W+音效库、50W+配乐库,全球版权授权(官网)", "time": "2026"}, + {"id": 22, "icon": "medal", "text": "PineSound发布企业标准 Q/YNPY 001-2026《数字内容创作 配乐通用分类标准》", "time": "2026"}, + {"id": 9, "icon": "award", "text": "2026年第一期入驻评审结果在昆明市人社局官网公示(23个优质项目正式入驻)", "time": "2026-03-27"}, + {"id": 10, "icon": "users", "text": "2026年第一期招募集中评审完成:38个申请,23个优质项目正式入驻", "time": "2026-03-24"}, + {"id": 19, "icon": "sparkles", "text": "云南派音人工智能科技专注多模态音频技术研发:自研Pine系列模型覆盖音频识别、向量嵌入、音效生成、配乐创作", "time": "2026-04"}, + {"id": 20, "icon": "bolt", "text": "「PineSound」2026年4月注册成立,获北京投资支持并吸纳就业4人,入驻加速区(入驻企业信息表)", "time": "2026-04"}, + {"id": 11, "icon": "cpu", "text": "云南首个人工智能OPC创新人才基地落地昆明,填补省内个体AI创业培育空白(公开报道)", "time": "2026-03-23"}, + {"id": 12, "icon": "medal", "text": "第九届\"春城创业荟\"创业创新大赛圆满闭幕,获奖项目名单公布", "time": "2025-09-30"}, + {"id": 13, "icon": "trophy", "text": "\"春城创业荟\"初赛落幕,147个项目晋级复赛", "time": "2025-04-28"}, + {"id": 14, "icon": "building", "text": "园内企业「花仙子园艺肥料(云南)」成立,注册资本118万元(公开报道)", "time": "2025-04-28"}, + {"id": 15, "icon": "radio", "text": "2025年入园项目评审结果公示(昆明市人社局官网)", "time": "2025-04-22"}, + {"id": 16, "icon": "atom", "text": "研X平台入选\"创客中国\"项目库(一站式研究生学术成长平台)", "time": "2025-01"}, + {"id": 17, "icon": "leaf", "text": "行业动态:园内「朵哈·玫瑰」原料产地昆明八街食用玫瑰行情上涨,玫瑰经济走强", "time": "2025-01"}, + {"id": 18, "icon": "heart", "text": "行业动态:「千楣之路」主营檀娜卡——泰国天然护肤品牌Thanaka正式进入中国市场", "time": "2025-01"}, +] + + +def _day_secs(): + """当日零点起已过秒数(时钟驱动,保证数值单向持续增长、重启连续)""" + now = time.time() + lt = time.localtime(now) + day_start = time.mktime((lt.tm_year, lt.tm_mon, lt.tm_mday, 0, 0, 0, 0, 0, -1)) + return max(0.0, now - day_start) + + +def _today_values(prev_noise=None): + """按真实速率推算当日累计值(单向增长 + 工具调用波动): + 平台实时速率 = 单会话 157 tok/s × 49 路并发 ≈ 7,700 t/s + 今日 TOKEN(万)= 秒数 × 7,700 / 10000 = 秒数 × 0.77 + 今日工具调用 = 今日 TOKEN / 15万 tokens 每次(真实基线) + + 随机游走噪声(±10,波浪式波动,不单调) + """ + secs = _day_secs() + today_wan = round(secs * 0.77) # 万 tokens + base_tools = secs * 0.77 * 10000 / REAL["tools_per_call_tokens"] # 真实基线(浮点) + if prev_noise is None: + noise = 0.0 + else: + noise = max(-10.0, min(10.0, prev_noise + (random.random() - 0.5) * 6.0)) + today_tools = max(0, round(base_tools + noise)) # 次 + return today_wan, today_tools, noise + + +def _make_series(base, noise, n=30): + """生成围绕日均锚点的演示曲线(单位:万 tokens),尾点=今日实时值""" + out = [] + for _ in range(n - 1): + v = base + (random.random() - 0.5) * noise + out.append(round(max(1, v), 1)) + out.append(round(base, 1)) + return out + + +def _live_values(prev=None): + """TOKEN 面板实时运行指标(围绕真实基准动态波动,会话持续推进)""" + r = random.random + steps = (prev or {}).get("steps", 81) + steps += 1 if r() < 0.3 else 0 + return { + "firstToken": round(1.3 + (r() - 0.5) * 0.2, 2), # 首 token 平均 1.3s 基准 ±0.1 + "throughput": round(157 + (r() - 0.5) * 14), # 吞吐 157 tok/s 基准 ±7 + "cacheHit": round(98 + (r() - 0.5) * 1.2, 1), # 缓存命中 98% 基准 ±0.6 + "inputM": round((prev or {}).get("inputM", 12.5) + 0.004 + r() * 0.008, 2), # 会话输入持续推进 + "outputM": round((prev or {}).get("outputM", 2.0) + 0.001 + r() * 0.002, 2), # 会话输出持续推进 + "rounds": max(2, steps // 40), # 每 40 步一轮 + "steps": steps, # 步数持续增长 + } + + +def init_snapshot(): + today_wan, today_tools, noise = _today_values() + return { + "as_of": REAL["as_of"], + "_toolsNoise": noise, + "live": _live_values(), + "token": { + "today": today_wan, # 万 tokens(当日累计,实时增长) + "total": REAL["token_monthly_yi"] * 10000 + today_wan, # 万 tokens(近30日 = 200亿 + 今日增量) + "rate": REAL["token_rate"], # t/s(157 tok/s × 49 并发 ≈ 7,700) + "series": _make_series(REAL["token_daily_wan"], REAL["token_daily_wan"] * 0.16), + }, + "tools": { + "today": today_tools, # 次(随 TOKEN 实时联动) + "total": round(REAL["token_monthly_yi"] * 100000000 / REAL["tools_per_call_tokens"]) + today_tools, # 次 + "success": 98.5, # 成功率(演示) + "nodes": 12, # AI 服务节点(演示) + }, + "projects": { + "inPark": REAL["occupied"], # 实际入驻 38 家(2026年7月统计表官方口径) + "capacity": REAL["capacity"], # 可容纳 49 个 + "invested": REAL["invested"], # 累计投入 560 万元 + }, + "jobs": {"total": REAL["jobs"]}, # 带动就业 213 人(当年累计口径) + "revenue": {"total": REAL["revenue_total"], "tax": REAL["revenue_tax"]}, # 万元(当年累计口径) + "cumulative": { # 省级绩效累计口径(附件1,截至2026年5月) + "incubated": REAL["incubated_total"], # 累计孵化企业 181 家 + "inIncubation": REAL["in_incubation"], # 当前实有在孵 31 家 + "graduated": REAL["graduated"], # 累计成功出园 149 家 + "patents": REAL["patents"], # 发明专利 171 项 + "jobs": REAL["jobs_total"], # 累计带动就业 1,529 人 + "revenue": REAL["revenue_total_cum"], # 累计经营收入 21,911.14 万元 + "tax": REAL["revenue_tax_cum"], # 累计税收 426.2 万元 + "mentors": REAL["mentors"], # 创业指导专家团队 50 人 + "successRate": REAL["success_rate"], # 近3年孵化成功率 95.91% + }, + "park": { + "area": REAL["area"], + "founded": REAL["founded"], + "provinceLevel": REAL["province_level"], + "address": REAL["address"], + "phone": REAL["phone"], + "email": REAL["email"], + }, + "zones": ZONES, + "companies": COMPANIES, + "industryMix": INDUSTRY_MIX, + "feed": FEED, + } + + +def next_snapshot(s): + """时钟驱动:按真实速率重算当日累计(TOKEN 单向增长,工具调用波浪波动),其余真实静态""" + today_wan, today_tools, noise = _today_values(s.get("_toolsNoise")) + token = { + "today": today_wan, + "total": REAL["token_monthly_yi"] * 10000 + today_wan, + "rate": REAL["token_rate"], + "series": s["token"]["series"][:-1] + [today_wan], + } + tools = { + "today": today_tools, + "total": round(REAL["token_monthly_yi"] * 100000000 / REAL["tools_per_call_tokens"]) + today_tools, + "success": 98.5, + "nodes": 12, + } + return { + **s, + "_toolsNoise": noise, + "live": _live_values(s.get("live")), + "token": token, + "tools": tools, + } + + +class SimEngine: + """带锁的快照引擎:单例供 API 与 MQTT tick 共用""" + + def __init__(self): + self._lock = threading.RLock() + self._snap = init_snapshot() + + def snapshot(self): + with self._lock: + return dict(self._snap) + + def tick(self): + with self._lock: + self._snap = next_snapshot(self._snap) + return dict(self._snap) + + +sim_engine = SimEngine() diff --git a/backend/app/storage.py b/backend/app/storage.py new file mode 100644 index 0000000..28f3a8d --- /dev/null +++ b/backend/app/storage.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +"""JSON 持久化:设置 / 播放列表 / URL 媒体(对齐原 Rust storage 语义)""" + +import json +import threading +import time +from pathlib import Path + +from .config import settings + +DEFAULT_SETTINGS = { + "username": "admin", + "password": "123456", + "volume": 80, + "sfx_volume": 60, + "auto_play": True, + "play_mode": "sequential", + "image_duration": 5, + "fullscreen": True, + "autostart": False, +} + + +class Storage: + def __init__(self, data_file: Path): + self.data_file = data_file + self._lock = threading.RLock() + self._data = self._load() + + # ---------- 底层 ---------- + def _load(self): + if self.data_file.exists(): + try: + data = json.loads(self.data_file.read_text("utf-8")) + # 规范化:URL 来源自动标记(兼容旧数据 source=None 的 URL 条目) + for it in data.setdefault("playlist", []): + p = it.get("path", "") + if p.startswith(("http://", "https://")) and not it.get("source"): + it["source"] = "url" + return data + except Exception: + pass + return {"settings": dict(DEFAULT_SETTINGS), "playlist": [], "url_media": []} + + def _save(self): + self.data_file.parent.mkdir(parents=True, exist_ok=True) + tmp = self.data_file.with_suffix(".tmp") + tmp.write_text(json.dumps(self._data, ensure_ascii=False, indent=2), "utf-8") + tmp.replace(self.data_file) + + # ---------- 设置 ---------- + def get_settings(self): + with self._lock: + s = dict(DEFAULT_SETTINGS) + s.update(self._data.get("settings", {})) + return s + + def update_settings(self, **kw): + with self._lock: + s = dict(DEFAULT_SETTINGS) + s.update(self._data.get("settings", {})) + for k, v in kw.items(): + if v is not None and k in s: + s[k] = v + self._data["settings"] = s + self._save() + + # ---------- 播放列表 ---------- + def get_playlist(self): + with self._lock: + return list(self._data.get("playlist", [])) + + def add_to_playlist(self, path): + with self._lock: + items = self._data.setdefault("playlist", []) + if not any(it.get("path") == path for it in items): + # URL 来源自动标记,避免被当作本地文件过滤 + source = "url" if path.startswith(("http://", "https://")) else None + items.append({"path": path, "source": source, "name": None}) + self._save() + + def remove_from_playlist(self, path): + with self._lock: + self._data.setdefault("playlist", []) + self._data["playlist"] = [it for it in self._data["playlist"] if it.get("path") != path] + self._save() + + # ---------- URL 媒体 ---------- + def get_url_media(self): + with self._lock: + return list(self._data.get("url_media", [])) + + def add_url_media(self, url, name, media_type): + """返回 True 表示新增,False 表示已存在(重复)""" + with self._lock: + items = self._data.setdefault("url_media", []) + if any(it.get("url") == url for it in items): + return False + items.append({ + "url": url, "name": name, "type": media_type, + "added_at": time.strftime("%Y-%m-%d %H:%M:%S"), + }) + self._save() + return True + + def delete_media(self, path): + with self._lock: + self._data.setdefault("url_media", []) + self._data["url_media"] = [it for it in self._data["url_media"] if it.get("url") != path] + self._data.setdefault("playlist", []) + self._data["playlist"] = [it for it in self._data["playlist"] if it.get("path") != path] + self._save() + + +storage = Storage(settings.DATA_FILE) diff --git a/backend/app/tools.py b/backend/app/tools.py new file mode 100644 index 0000000..e0562d2 --- /dev/null +++ b/backend/app/tools.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +"""智能体工具注册表 —— LLM function calling 的后端执行器 + +- TOOLS:工具名 → 描述 + JSON Schema 参数(给 LLM 声明用) +- exec_tool(name, args):执行工具,返回给 LLM 的字符串结果(JSON) +- 供 POST /api/tools/exec 调用;前端 s2s 客户端收到 toolcall 事件后转发到此执行 +""" +import json +import logging +from datetime import datetime + +log = logging.getLogger("dpm.tools") + + +def _trunc(s, n=800): + """日志用截断:超长内容只留前 n 字并标注总长。""" + s = str(s) + return s if len(s) <= n else s[:n] + f"...(共{len(s)}字)" + + +# ── 工具实现 ──────────────────────────────────────────────────────────── + +def _get_park_overview(args): + """园区实时概览(官方统计口径 + 省级绩效累计口径 + AI 平台运行指标)""" + from .sim_engine import sim_engine + d = sim_engine.snapshot() + return json.dumps({ + "数据口径": "官方统计(《2026年7月运行情况统计表》);累计口径(附件1省级材料,截至2026年5月);AI平台指标:TOKEN/工具调用", + "实际入驻企业数(家)": d["projects"]["inPark"], + "园区可容纳企业数(个)": d["projects"]["capacity"], + "开园以来累计投入运营资金(万元)": d["projects"]["invested"], + "带动就业人数(人,当年累计)": d["jobs"]["total"], + "生产经营总额(万元,当年累计)": d["revenue"]["total"], + "上缴税利总额(万元,当年累计)": d["revenue"]["tax"], + "累计孵化企业(家,省级绩效累计)": d["cumulative"]["incubated"], + "当前实有在孵实体(家,省级绩效累计)": d["cumulative"]["inIncubation"], + "累计成功孵化出园企业(家,开园以来累计)": d["cumulative"]["graduated"], + "入驻团队发明专利(项,累计)": d["cumulative"]["patents"], + "累计带动就业(人,省级绩效累计)": d["cumulative"]["jobs"], + "入驻团队累计经营收入(万元)": d["cumulative"]["revenue"], + "入驻团队累计税收(万元)": d["cumulative"]["tax"], + "创业指导专家团队(人)": d["cumulative"]["mentors"], + "近3年入孵实体孵化成功率(%)": d["cumulative"]["successRate"], + "园区建筑面积(㎡)": d["park"]["area"], + "TOKEN月均消耗(亿)": 200, + "TOKEN实时速率(t/s)": d["token"]["rate"], + "今日TOKEN消耗(万)": d["token"]["today"], + }, ensure_ascii=False) + + +def _query_companies(args): + """园区入驻企业名录查询(关键词过滤)""" + from .sim_engine import COMPANY_NAMES + q = (args.get("keyword") or "").strip() + names = [n for n in COMPANY_NAMES if q in n] if q else list(COMPANY_NAMES) + return json.dumps({ + "total": len(names), + "keyword": q, + "companies": names[:20], + }, ensure_ascii=False) + + +def _control_display(args, screen_id=""): + """大屏显示控制:切页 / 弹通知 / 媒体播放暂停(经 MQTT 按发起屏幕下发)""" + from .mqtt import hub + action = (args.get("action") or "").strip() + target = (args.get("target") or "").strip() + if action not in ("switch_page", "alert", "media_play", "media_pause"): + return json.dumps({"error": f"不支持的 action: {action}"}, ensure_ascii=False) + params = {"target": target} if target else {} + ok = hub.publish_command(action, params, screen_id=screen_id).get("published", False) + return json.dumps({"ok": ok, "action": action, "target": target}, ensure_ascii=False) + + +def _get_time(args): + """当前日期时间""" + return json.dumps({"datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}, ensure_ascii=False) + + +# ── 注册表 ────────────────────────────────────────────────────────────── + +TOOLS = { + "get_park_overview": { + "fn": _get_park_overview, + "description": "获取园区实时运营概览:在园项目数、累计孵化企业、带动就业、营收(今日/累计)、设备在线率、在园人数、能耗等。回答园区数据类问题时使用。", + "parameters": {"type": "object", "properties": {}}, + }, + "query_companies": { + "fn": _query_companies, + "description": "查询园区入驻企业名录,支持按企业名关键词过滤。回答'有哪些企业/某企业是否入驻'时使用。", + "parameters": { + "type": "object", + "properties": {"keyword": {"type": "string", "description": "企业名关键词,可为空字符串"}}, + }, + }, + "control_display": { + "fn": _control_display, + "description": "控制大屏显示:switch_page 切换页面、alert 弹通知、media_play/media_pause 控制媒体播放。用户要求控制大屏时使用。", + "parameters": { + "type": "object", + "properties": { + "action": {"type": "string", "enum": ["switch_page", "alert", "media_play", "media_pause"]}, + "target": {"type": "string", "description": "目标:页面路径(如 /、/twin、/ai、/voice)或通知文本"}, + }, + }, + }, + "get_time": { + "fn": _get_time, + "description": "获取当前日期时间。用户问'现在几点/今天几号'时使用。", + "parameters": {"type": "object", "properties": {}}, + }, +} + + +def tool_schemas(): + """给 LLM 的 tools 定义(OpenAI function calling 格式)""" + return [ + { + "type": "function", + "name": name, + "description": t["description"], + "parameters": t["parameters"], + } + for name, t in TOOLS.items() + ] + + +def exec_tool(name: str, args: dict, screen_id: str = ""): + """执行工具 → 返回给 LLM 的字符串结果(JSON)。 + 控制类工具(control_display)按发起屏幕 screen_id 只下发到该屏。""" + t = TOOLS.get(name) + if not t: + log.warning("tools: 未知工具 %s", name) + return json.dumps({"error": f"未知工具: {name}"}, ensure_ascii=False) + try: + if name == "control_display": + result = _control_display(args or {}, screen_id) + else: + result = t["fn"](args or {}) + log.info("tools: 执行工具 %s args=%s screen=%s", name, json.dumps(args, ensure_ascii=False), screen_id or "all") + log.info("tools: 结果 -> %s", _trunc(result, 1000)) + return result + except Exception as e: # noqa: BLE001 + log.error("tools: %s 执行失败 %s", name, e, exc_info=True) + return json.dumps({"error": f"{name} 执行失败: {e}"}, ensure_ascii=False) diff --git a/backend/app/vision_llm.py b/backend/app/vision_llm.py new file mode 100644 index 0000000..6d653f1 --- /dev/null +++ b/backend/app/vision_llm.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +"""对话开场画面识别 —— 用多模态 LLM(qwen3-vl-flash)识别实时画面中的人数、性别等, +并把结果作为上下文注入对话,让 AI 了解当前在场观众。 + +与 vision_yolo(YOLO 数人脸/姿态)互补:这里用 LLM 做语义级理解(人数、性别构成、场景描述)。 +""" +import json +import logging +import ssl +import urllib.request + +import certifi + +from .config import settings + +log = logging.getLogger("dpm.vision") + +_SSL_CTX = ssl.create_default_context(cafile=certifi.where()) + +_DEFAULT_PROMPT = ( + "请识别这张实时画面,只输出一个 JSON 对象(不要输出任何其他文字):" + '{"people": 画面中人数(int), "males": 其中男性人数(int), "females": 其中女性人数(int), ' + '"desc": 一句话中文描述画面(含大致人数、性别构成、人物大致状态,如年龄/坐站/是否看屏幕)。' + "若画面无人或不确定,则 people=0、males=0、females=0,desc='画面中暂时没有人'。" +) + + +def analyze_scene(jpeg_b64: str, prompt: str = "") -> dict: + """调用多模态 LLM 识别画面中人数/性别,返回 {ok, people, males, females, desc}。""" + payload = { + "model": settings.VISION_LLM_MODEL, + "messages": [{ + "role": "user", + "content": [ + {"type": "text", "text": prompt or _DEFAULT_PROMPT}, + {"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{jpeg_b64}"}}, + ], + }], + } + req = urllib.request.Request( + f"{settings.LLM_BASE_URL}/chat/completions", + data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}", + }, + ) + with urllib.request.urlopen(req, timeout=30, context=_SSL_CTX) as resp: + data = json.loads(resp.read().decode("utf-8")) + content = data["choices"][0]["message"].get("content") or "" + return _parse(content) + + +def _parse(text: str) -> dict: + """从模型输出中抽取 JSON 对象并规范化为结果 dict。""" + text = (text or "").strip() + if text.startswith("```"): + lines = [l for l in text.splitlines() if not l.strip().startswith("```")] + text = "\n".join(lines).strip() + obj: dict = {} + i, j = text.find("{"), text.rfind("}") + if i != -1 and j > i: + try: + obj = json.loads(text[i:j + 1]) + except Exception: # noqa: BLE001 + log.warning("vision llm: 返回 JSON 解析失败: %s", text[:120]) + people = _as_int(obj.get("people")) + males = _as_int(obj.get("males")) + females = _as_int(obj.get("females")) + return { + "ok": True, + "people": people, + "males": males, + "females": females, + "desc": str(obj.get("desc") or "").strip(), + } + + +def _as_int(v): + try: + return int(float(v)) + except (TypeError, ValueError): + return 0 diff --git a/backend/app/vision_yolo.py b/backend/app/vision_yolo.py new file mode 100644 index 0000000..51cec0b --- /dev/null +++ b/backend/app/vision_yolo.py @@ -0,0 +1,204 @@ +# -*- coding: utf-8 -*- +"""YOLO 人脸检测 + 姿态估计(后端推理) + +链路:前端摄像头抽帧 → JPEG base64 → POST /api/vision/frame → 本模块推理 → 返回人脸/姿态/举手特征。 +模型: + - backend/models/yolov8n-face.pt (lindevs/yolov8-face,WIDERFACE 训练,torch 6.3MB) + - backend/models/yolov8n-pose.pt (ultralytics 官方,COCO 17 关键点,torch 6.8MB) +推理:ultralytics 加载(torch 原生,自动 letterbox + NMS),CPU 设备。 + +手势判定分工: + - 后端只做"单帧几何特征"(举手 = 手腕高于肩),无状态 + - 前端做时序判定(举手持续 1.5s → 开始对话;举手后手腕摆动 → 挥手结束对话) +""" +import base64 +import io +import logging +import time +from pathlib import Path + +from PIL import Image + +log = logging.getLogger("dpm.vision") + +MODEL_DIR = Path(__file__).resolve().parent.parent / "models" +FACE_MODEL_PATH = MODEL_DIR / "yolov8n-face.pt" +POSE_MODEL_PATH = MODEL_DIR / "yolov8n-pose.pt" +CONF_THRESHOLD = 0.25 # 远距离小脸可调低(如 0.15) +IMGSZ = 640 + +# COCO 17 关键点索引 +KP_LEFT_SHOULDER = 5 +KP_RIGHT_SHOULDER = 6 +KP_LEFT_ELBOW = 7 +KP_RIGHT_ELBOW = 8 +KP_LEFT_WRIST = 9 +KP_RIGHT_WRIST = 10 +RAISE_LIFT = 0.06 # 举手:手腕高于对应肩 6% 画面高(降低阈值,更易稳定激活) +FIST_ELBOW_LIFT = 0.05 # 举拳:手腕高于肘 5% 画面高(前臂上举) +FIST_SHOULDER_GAP = 0.15 # 举拳:拳不高于肩 15% 画面高(收在胸前) +FIST_CHEST_DIST = 0.25 # 举拳:拳与肩水平距离 < 25% 画面宽(贴近躯干) +KP_CONF_MIN = 0.3 # 关键点置信度过滤 + +# ── 扩展手势几何阈值(基于 COCO 17 关键点) ── +POINT_REACH_X = 0.30 # 指向:手腕水平伸出距肩 ≥30% 画面宽(手臂向前/侧伸) +POINT_Y_RANGE = 0.28 # 指向:手腕与肩同高 ±28% 画面高(排除高举/下垂) +HANDS_CLOSE_DIST = 0.18 # 双手合十/靠近:双腕欧氏距离 <18% 画面宽 + +_face_model = None +_pose_model = None + + +def get_face_model(): + global _face_model + if _face_model is None: + from ultralytics import YOLO + log.info("vision: 加载 YOLO 人脸模型 %s(首次约 2-5s)", FACE_MODEL_PATH) + _face_model = YOLO(str(FACE_MODEL_PATH), task="detect") + log.info("vision: YOLO 人脸模型就绪") + return _face_model + + +def get_pose_model(): + global _pose_model + if _pose_model is None: + from ultralytics import YOLO + log.info("vision: 加载 YOLO 姿态模型 %s(首次约 2-5s)", POSE_MODEL_PATH) + _pose_model = YOLO(str(POSE_MODEL_PATH), task="pose") + log.info("vision: YOLO 姿态模型就绪") + return _pose_model + + +def _parse_pose(results, W, H): + """解析姿态结果 → persons + raised(举手) + fists(举拳) + + both_up(双臂举起) + pointing(指向) + hands_close(双手合十) + + hands(每手几何摘要,供前端挥手/手势跟随时序判定) + 注意:ultralytics keypoints.data 为【原图像素坐标】, + 此处统一归一化为 [0,1](x/W, y/H)后再判定阈值与输出 + """ + persons = [] + raised = [] + fists = [] + pointing = [] + hands = [] + for r in results: + if r.keypoints is None: + continue + kps = r.keypoints.data # [N,17,3] 像素坐标 + for i in range(kps.shape[0]): + kp = kps[i] + person = [ + [round(float(kp[j][0]), 3), round(float(kp[j][1]), 3), round(float(kp[j][2]), 3)] + for j in range(17) + ] + persons.append(person) + # 每只手臂:举手(腕明显高于肩)或 举拳(前臂上举、拳收胸前),两者互斥 + arm = {} + for side, wrist_i, elbow_i, shoulder_i in ( + ("left", KP_LEFT_WRIST, KP_LEFT_ELBOW, KP_LEFT_SHOULDER), + ("right", KP_RIGHT_WRIST, KP_RIGHT_ELBOW, KP_RIGHT_SHOULDER), + ): + w = kp[wrist_i] + e = kp[elbow_i] + s = kp[shoulder_i] + if float(w[2]) < KP_CONF_MIN or float(s[2]) < KP_CONF_MIN or float(e[2]) < KP_CONF_MIN: + continue + # 像素 → 归一化 [0,1] + wx, wy = float(w[0]) / W, float(w[1]) / H + sx, sy = float(s[0]) / W, float(s[1]) / H + ey = float(e[1]) / H + is_raised = wy < sy - RAISE_LIFT + is_fist = ( + wy < ey - FIST_ELBOW_LIFT + and wy >= sy - FIST_SHOULDER_GAP + and abs(wx - sx) < FIST_CHEST_DIST + ) + if is_raised: + raised.append({"side": side, "x": round(wx, 4), "y": round(wy, 4)}) + elif is_fist: + fists.append({"side": side, "x": round(wx, 4), "y": round(wy, 4)}) + # 指向:手腕水平伸出距肩较远、且与肩同高区间(前伸/侧伸,排除高举与下垂) + if ( + abs(wx - sx) > POINT_REACH_X + and abs(wy - sy) < POINT_Y_RANGE + ): + pointing.append({"side": side, "x": round(wx, 4), "y": round(wy, 4)}) + arm[side] = { + "side": side, + "wx": round(wx, 4), "wy": round(wy, 4), + "sx": round(sx, 4), "sy": round(sy, 4), + "raised": is_raised, "fist": is_fist, + } + if arm: + hands.append(arm) + # 双臂举起:左、右腕都高于各自肩 + both_up = False + if len(hands) >= 1: + h0 = hands[0] + if "left" in h0 and "right" in h0: + both_up = h0["left"]["raised"] and h0["right"]["raised"] + # 双手合十/靠近:同一人的双腕欧氏距离 < 阈值 + hands_close = False + if len(hands) >= 1: + h0 = hands[0] + if "left" in h0 and "right" in h0: + dx = h0["left"]["wx"] - h0["right"]["wx"] + dy = h0["left"]["wy"] - h0["right"]["wy"] + hands_close = (dx * dx + dy * dy) ** 0.5 < HANDS_CLOSE_DIST + return persons, raised, fists, both_up, pointing, hands_close, hands + + +def predict_jpeg(jpeg_bytes: bytes): + """人脸检测 + 姿态估计 → {faces, boxes, pose, raised, fists, both_up, pointing, hands_close, hands, latency_ms}""" + t0 = time.time() + img = Image.open(io.BytesIO(jpeg_bytes)).convert("RGB") + + # 人脸 + face_res = get_face_model().predict(img, conf=CONF_THRESHOLD, imgsz=IMGSZ, verbose=False, device="cpu") + boxes = [] + for r in face_res: + if r.boxes is None: + continue + for b in r.boxes: + xyxy = [round(float(v), 1) for v in b.xyxy[0].tolist()] + conf = round(float(b.conf[0]), 3) + boxes.append({"box": xyxy, "conf": conf}) + + # 姿态 + W, H = img.size # 原图尺寸(关键点像素坐标 → 归一化基准) + pose_res = get_pose_model().predict(img, conf=CONF_THRESHOLD, imgsz=IMGSZ, verbose=False, device="cpu") + persons, raised, fists, both_up, pointing, hands_close, hands = _parse_pose(pose_res, W, H) + + latency_ms = round((time.time() - t0) * 1000, 1) + log.info( + "vision: 人脸 %d 姿态 %d 举手 %d 举拳 %d 指向 %d 合十 %s · %.0fms", + len(boxes), len(persons), len(raised), len(fists), len(pointing), hands_close, latency_ms, + ) + return { + "faces": len(boxes), + "boxes": boxes, + "pose": persons, + "raised": raised, + "fists": fists, + "both_up": both_up, + "pointing": pointing, + "hands_close": hands_close, + "hands": hands, + "latency_ms": latency_ms, + } + + +def predict_base64(b64: str): + """入口:base64 JPEG → 检测结果 dict(含 ok 标记,失败时带 error)""" + try: + jpeg = base64.b64decode(b64) + except Exception as e: # noqa: BLE001 + log.warning("vision: base64 解码失败 %s", e) + return {"ok": False, "error": f"bad base64: {e}"} + try: + data = predict_jpeg(jpeg) + data["ok"] = True + return data + except Exception as e: # noqa: BLE001 + log.error("vision: 推理失败 %s", e, exc_info=True) + return {"ok": False, "error": str(e)} diff --git a/backend/docker_entry.py b/backend/docker_entry.py new file mode 100644 index 0000000..608267f --- /dev/null +++ b/backend/docker_entry.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +"""Docker 生产入口 —— 与 main.py 环境初始化完全一致,但关闭 reload。 + +用法(镜像内):python docker_entry.py +- import main 触发 prepare_env():读取 backend/.env、设置 TORCH_HOME/NLTK_DATA/ + NO_PROXY/SSL_CERT_FILE、默认开启 s2s(可用环境变量 DPM_S2S_ENABLED=0 关闭) +- 再以 uvicorn 非 reload 模式启动 app.main:app +""" +import main # noqa: F401 (执行 prepare_env 与环境初始化) + +import uvicorn + +from app.config import settings + +if __name__ == "__main__": + uvicorn.run( + "app.main:app", + host="0.0.0.0", + port=int(settings.PORT), + reload=False, + workers=1, + access_log=True, + ) diff --git a/backend/knowledge/kb_meta.json b/backend/knowledge/kb_meta.json new file mode 100644 index 0000000..59b2817 --- /dev/null +++ b/backend/knowledge/kb_meta.json @@ -0,0 +1 @@ +{"model": "text-embedding-v4", "data_hash": "6503b6c923d23ce4772c6fceb3f4cc0eb53db77f", "chunks": ["> 口径说明:以下政策与数据基于公开资料整理,截至 2026 年 8 月。涉及具体补贴金额、名额、申报条件时,以云南省及昆明市最新官方文件为准;本库作为语义检索与回答口径的参考。", "一、什么是 OPC(一人公司)\n- 定义:一人公司(One Person Company, OPC)指只有一个股东的公司,分为一人有限责任公司和一人股份有限公司两类。", "一、什么是 OPC(一人公司)\n- 法律沿革:传统公司法理论认为公司属社团法人、应有 2 人以上股东;我国自 2005 年修订《公司法》起正式引入一人有限责任公司。", "一、什么是 OPC(一人公司)\n- 时代定位:在 AI 时代,一人公司成为低成本、高效率、普惠型的创业形态——创业者仅凭一人之力,借助 AI 工具即可构建覆盖产品研发、市场营销、客户服务与财务管理的完整业务闭环。", "一、什么是 OPC(一人公司)\n- 核心理念:「你负责商业判断,AI 负责高效执行」,实现从\"出卖单次时间\"到\"经营自动化系统\"的跨越。", "一、什么是 OPC(一人公司)\n- 与自由职业的区别:一人公司具备单一股东、有限责任、极简架构等正规企业属性,同时兼具灵活高效、税负更可控等优势。", "一、什么是 OPC(一人公司)\n- 规模:截至 2025 年 6 月,全国一人有限责任公司已超过 1600 万家。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 总特征:准入放宽与责任强化并存。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 允许设立一人股份有限公司:设立股份有限公司应有一人以上二百人以下发起人,一名股东也可设立股份有限公司。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 取消一人股东数量限制:删除旧法\"一个自然人只能投资设立一个一人有限责任公司,且该一人公司不能投资设立新的一人公司\"的规定;一个自然人可设多个一人公司,可形成投资链条。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 放宽股东类型:删除\"一人公司股东只能是自然人或者法人\"的限制,个体工商户、合伙企业、个人独资企业等理论上也可作股东设立一人公司。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除专节规定:不再采用\"一人有限责任公司\"或\"一人股份有限公司\"的特殊表述,统一为\"只有一个股东的公司\",纳入普通公司规制。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除年度审计强制要求:不再强制一人公司每年编制财务会计报告并审计。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除登记特别注明:不再要求在登记及营业执照中注明\"自然人独资\"或\"法人独资\"。", "(责任强化,需重点注意)", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 举证责任倒置:新《公司法》第 23 条第 3 款规定,只有一个股东的公司,股东不能证明公司财产独立于股东自己财产的,应对公司债务承担连带责任。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 财产混同是主要认定依据:司法实践中,公司账户与股东个人账户资金往来频繁是财产混同的主要认定依据。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- AI 时代\"数字资产混同\"风险:用个人名义注册付费订阅 AI 大模型、购买云端算力、将训练数据与生成物存于个人账号,一旦发生纠纷,可能被认定公司缺乏独立财产基础,判令股东承担连带责任。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 不设股东会时须书面决定:股东就利润分配、亏损弥补、修改章程等事项,须以书面形式决定,签名或盖章后置备于公司。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 建立健全财务管理制度,账目清晰、独立核算,严格区分公司与个人财产界限。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 适时出具连续完整的会计年度审计报告,或股东与公司往来专项审计报告,作为财产独立性的有力证据。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 将合规视野从\"财务隔离\"拓展到\"数据与数字资产隔离\"。", "三、OPC 创业思路与经验\n- 先完成,再完美:AI 已把门槛拆掉,不要等学会所有技能再开始。", "三、OPC 创业思路与经验\n- 最小成本验证模式:用 AI 思考与决策,用外包解决人力,用最小成本验证商业假设。", "三、OPC 创业思路与经验\n- 聚焦四力:产品能力(解决什么问题)、流量能力(客户如何找到你)、交付能力(如何高效完成服务)、变现能力(如何设计商业模式)。", "(盈利三把钥匙与商业模式)", "三、OPC 创业思路与经验\n- 盈利三把钥匙:用 AI 降本增效、用信任创造溢价、用平台借势腾飞。", "三、OPC 创业思路与经验\n- 六大主流商业模式:知识付费(课程/咨询/私教)、内容变现(公众号/小红书/抖音)、服务外包(代运营/代写/代设计)、工具产品(模板/插件/SaaS)、社群运营(付费社群/会员制)、混合模式。", "三、OPC 创业思路与经验\n- 从盘点个人技能 → 验证市场需求 → 找准差异化定位 → 用极低成本测试 MVP → 搭建自动化内容引流矩阵与私域池 → 打造标准化交付质控系统 → 设计高溢价产品矩阵 → 构建抗风险的多元化收入线。", "三、OPC 创业思路与经验\n- 电商:河南创业者卧床 70 天,用 AI 替代原计划的 10 人团队做跨境电商,开出 12 家网店(后优化至 4 家主力店),单店月销 5-8 万、月净利 2.5-3 万,每天只需工作 2-3 小时;用扣子搭工作流自动生成经营日报。", "三、OPC 创业思路与经验\n- 内容与服务:无锡创业者 2025 年转型\"企业陪跑\",帮企业做小红书内容转化;指出 OPC 两大难题是\"如何定价\"与\"如何建立交易机制(个人即产品、前期缺乏标准化交付物)\"。", "三、OPC 创业思路与经验\n- 全 AI 员工公司:创业者用开源 AI 代理框架创建\"零人类公司\",业务含卖教程、AI 技能市场抽佣、企业定制代理,几周收入近 20 万美元,运营成本仅约每月 1500 美元。", "三、OPC 创业思路与经验\n- 大厂裸辞:35 岁管理者裸辞环游多国,靠 AI 创立一人公司,兼任大模型工具开发者、企业咨询顾问、自媒体博主,年入近百万。", "(清醒认知与挑战)", "三、OPC 创业思路与经验\n- AI 没有消灭赚钱的门槛:认知、决策能力甚至运气同样重要;没有商业逻辑,再好的工具也没用。", "三、OPC 创业思路与经验\n- 理性看待收入神话:网上\"一人公司月入十万\"的故事大多不靠谱,成功相当程度依赖运气。", "三、OPC 创业思路与经验\n- 一人公司的孤独感与亲力亲为:现金流、回款、定价等都需要自己承担。", "(省级专项:2026 年 7 月云南省人社厅《关于支持服务人工智能 OPC 创业的若干措施》)", "四、云南当前生效的 OPC 超级个体政策\n- 创业生态社区:鼓励盘活闲置楼宇、高校实训场地等,整合算力、绿电、数据、市场渠道等要素,打造\"低成本空间+普惠算力+全链条生态服务\"的 AI OPC 创业生态社区;运营效果好的可按创业孵化载体补助政策认定补助。", "四、云南当前生效的 OPC 超级个体政策\n- 免费工位与安居支持:支持现有创业园区开设 OPC 专区,提供拎包入驻免费工位;有条件地区为 OPC 创业人才提供免租或低租人才公寓、青年驿站。", "四、云南当前生效的 OPC 超级个体政策\n- 金融支持:\"云岭创业贷\"——企业法人最高可申请 500 万元信用贷款、3000 万元抵押担保贷款;个人创业者最高可申请 50 万元信用贷款、1000 万元抵押担保贷款;鼓励推出\"算力贷\"\"数智创新专项信贷\"等专属产品。", "四、云南当前生效的 OPC 超级个体政策\n- 创业补贴:符合条件者给予 5000 元一次性创业补贴;电商创业项目每个最高补贴 3 万元。", "四、云南当前生效的 OPC 超级个体政策\n- 见习补贴:吸纳高校毕业生就业见习的按每人每月 1500 元(留用率达 50% 以上可增至 2000 元)发放见习补贴。", "四、云南当前生效的 OPC 超级个体政策\n- 培训补贴:AI 相关职业(如数据标注员)纳入培训补贴范围,最高可获 1700 元创业培训补贴和 7280 元技能培训补贴。", "四、云南当前生效的 OPC 超级个体政策\n- 人才政策:符合条件的 AI OPC 创始人纳入\"兴滇英才支持计划\"等省级人才计划;设立人工智能专业职称评审;打破学历、年龄、资历限制建立人才自主评价机制。", "四、云南当前生效的 OPC 超级个体政策\n- 政务服务:深化\"个人创业一件事\"集成办理,实现 OPC 企业登记一表申请、一网联办、一次办结。", "(市级:昆明市大学生创业园)", "四、云南当前生效的 OPC 超级个体政策\n- 面向对象:在校大学生、毕业不超过 8 年的高校毕业生、归国及外籍留学生等青年创业者。", "四、云南当前生效的 OPC 超级个体政策\n- 入驻方向:项目须聚焦昆明市\"10+N\"产业链(人工智能、数字经济、电子信息、新媒体直播、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等);获市级以上创业大赛奖项、有突出学术成就或高新专利者优先。", "四、云南当前生效的 OPC 超级个体政策\n- 空间保障:免费提供办公工位或独立区域。", "四、云南当前生效的 OPC 超级个体政策\n- 孵化服务:专家一对一创业指导、商业模式梳理、沙龙路演。", "四、云南当前生效的 OPC 超级个体政策\n- 融资支持:联动创投机构设立种子资金,每年组织不少于 15 场融资对接活动。", "四、云南当前生效的 OPC 超级个体政策\n- 政策对接:协助申请创业扶持资金、税收减免、社保补贴。", "四、云南当前生效的 OPC 超级个体政策\n- 资源整合:对接高校、科研院所、龙头企业。", "四、云南当前生效的 OPC 超级个体政策\n- 专项赋能:免费提供\"AI 育笋计划\"和\"AI+OPC\"全套培训(涵盖 AI 应用、一人公司落地、成本控制等)。", "四、云南当前生效的 OPC 超级个体政策\n- 综合服务:工商注册、税务代办、知识产权咨询、法律援助一站式支持。", "(OPC 核心范式)", "五、OPC 的几种创业模式\n- OPC 的核心是「1 个创始人 + N 个 AI 数字员工」的 1+N 模式:创始人负责战略决策、创意设计与关键价值判断,AI 承担代码生成、内容创作、数据运营、客户服务等标准化重复工作。", "五、OPC 的几种创业模式\n- OPC 将创业从「资本密集型」转向「认知密集型」:无需融资和组建团队,只需对领域有深度认知,再用 AI 将其转化为产品、服务或收入。", "五、OPC 的几种创业模式\n- 创业门槛从「资源门槛」降为「认知门槛」:代码可用 Claude Code 写、内容可用 AI 生成、跨语言可实时翻译、客户对接可用 AI Agent 筛选。", "(六大已跑通的 OPC 商业模式)", "五、OPC 的几种创业模式\n- 专业服务型:以深度专业技能为核心价值,交付定制化解决方案或咨询,按项目或小时收费,利润率高。", "五、OPC 的几种创业模式\n- 高端服务型:以行业认知与战略判断力为核心,提供高端顾问、私教、战略规划,高客单(一单可达几十万)。", "五、OPC 的几种创业模式\n- 知识产品型:将系统化专业知识做成标准化数字产品(课程、电子书、会员),边际交付成本趋近于零,最被看好。", "五、OPC 的几种创业模式\n- 数字产品型:可复用的软件或数字工具(SaaS、应用、插件、模板),订阅制或一次性/API 收费,规模化潜力最大。", "五、OPC 的几种创业模式\n- 创意内容型:靠个人影响力或 IP,产出博客、视频、社媒内容,通过广告分成、品牌合作、知识付费变现。", "五、OPC 的几种创业模式\n- 平台整合型:做资源连接与撮合,自身不生产产品,靠佣金或撮合费。", "(如何选择适合你的模式)", "五、OPC 的几种创业模式\n- 先想清三个问题:卖「能力」还是卖「系统」?服务边界清不清楚?你是「执行者」还是「系统设计师」?", "五、OPC 的几种创业模式\n- 顾问/咨询型适合专业壁垒深厚、客户关系强的人;AI 可解放线索收集、标准问答、方案初稿。", "五、OPC 的几种创业模式\n- 内容/知识型放大个人 IP,内容可标准化复用、多平台分发,边际成本趋零,适合有表达与知识结构化能力的人。", "五、OPC 的几种创业模式\n- 产品化服务型(SaaS/数字产品)将方法论打包成模板或工具,从「卖服务」转向「卖产品」,长期复利最可观但技术含量最高。", "五、OPC 的几种创业模式\n- 热门赛道:健康/医疗/心理支持、硬件/IoT/机器人、教育/培训/知识服务、金融/法律/政务等专业服务、企业服务/SaaS。", "六、如何成立一家 OPC(一人公司)\n- 先副业后主体:不建议立即辞职,先以副业验证模式。", "六、如何成立一家 OPC(一人公司)\n- 个体工商户注册快、税负低,适合早期验证、接小单;有限责任公司信用强、利于 B 端,适合长期发展,但务必公私分明避免财产混同。", "六、如何成立一家 OPC(一人公司)\n- 若业务面向 B 端长期合作选有限公司,早期验证可选个体户。", "六、如何成立一家 OPC(一人公司)\n- 公司名称核准:提交 3-5 个备选名称(行政区划+字号+行业+有限公司),经市场监管局官网或「一网通办/一窗通」平台核准。", "六、如何成立一家 OPC(一人公司)\n- 提交设立登记申请:线上提交公司章程、股东身份证明、注册地址证明等,约 3-5 个工作日领取营业执照。", "六、如何成立一家 OPC(一人公司)\n- 刻制印章:公章、财务章、法人章,到公安备案刻章点办理,费用约 500 元。", "六、如何成立一家 OPC(一人公司)\n- 银行开户:预约开基本户,需带执照、公章、法人身份证,年费约 500-1000 元。", "六、如何成立一家 OPC(一人公司)\n- 税务登记:领取执照后 30 日内办理,核定税种、申请发票。", "六、如何成立一家 OPC(一人公司)\n- 新《公司法》允许单人担任法人且无需监事。", "(注册资本与地址)", "六、如何成立一家 OPC(一人公司)\n- 实行认缴制,注册时无需实缴全部资金;但须在章程明确认缴期限,且 5 年内实缴到位。", "六、如何成立一家 OPC(一人公司)\n- 认缴≠不缴:切忌盲目追高注册资本,经营不善时股东需在认缴范围内承担责任。", "六、如何成立一家 OPC(一人公司)\n- 注册地址须为商用地址(写字楼、商铺、共享办公工位、集中登记地/虚拟地址等),住宅原则上不可用。", "六、如何成立一家 OPC(一人公司)\n- 可租用园区或共享工位,或用集中登记地/虚拟地址(部分园区免费或年费约 3000-6000 元)。", "(记账报税与合规)", "六、如何成立一家 OPC(一人公司)\n- 公司必须建立账簿,即使零收入也须按月或按季申报(零申报),逾期可能被列入经营异常名录。", "六、如何成立一家 OPC(一人公司)\n- 小规模纳税人季度开票≤30 万免增值税(普票);小微企业年应纳税所得额≤300 万按 5% 缴企业所得税(2027 年底前)。", "六、如何成立一家 OPC(一人公司)\n- 一人公司利润分配后转账需缴 20% 分红个税。", "六、如何成立一家 OPC(一人公司)\n- 代理记账费用约 200-500 元/月,起步阶段建议找代账公司。", "六、如何成立一家 OPC(一人公司)\n- 每年 6 月 30 日前登录「国家企业信用信息公示系统」提交年报。", "六、如何成立一家 OPC(一人公司)\n- 严格区分公司财产与股东个人财产,避免公私账户混同,否则股东可能对公司债务承担连带责任。", "七、如何选用合适的 AI 工具\n- 按「场景适配」选工具,不要盲目追新:没有哪一款模型在所有任务上全面领先。", "七、如何选用合适的 AI 工具\n- OPC 中 AI 首先承担软件开发角色(多数团队用 AI 编程),其次才是内容创作。", "七、如何选用合适的 AI 工具\n- 优先选生态开放、能接 API/插件、支持工作流编排的平台,方便搭建自动化。", "七、如何选用合适的 AI 工具\n- 写代码、推理、性价比选 DeepSeek(开源、API 定价低)或智谱 GLM(编程强、可私有化)。", "七、如何选用合适的 AI 工具\n- 中文通用与云生态选通义千问(阿里云百炼 MaaS、长上下文)。", "七、如何选用合适的 AI 工具\n- 多模态、免费 C 端选豆包(字节),创意内容可用其视频/图像生成。", "七、如何选用合适的 AI 工具\n- 长文档、研报处理选 Kimi(超长上下文)。", "七、如何选用合适的 AI 工具\n- 政企、资质、央国企场景选文心一言(百度,合规资质全)或讯飞星火(语音、信创)。", "七、如何选用合适的 AI 工具\n- 微信生态、快慢思考选腾讯混元。", "(Agent 与工作流搭建平台)", "七、如何选用合适的 AI 工具\n- 扣子(Coze):字节推出的零代码 Agent 平台,拖拽式搭建,适合个人/非技术用户,支持豆包、DeepSeek、Kimi、通义千问、GLM 等模型。", "七、如何选用合适的 AI 工具\n- Dify:企业级、技术向,工作流节点丰富,支持国内外主流模型与私有化部署。", "七、如何选用合适的 AI 工具\n- 用无代码平台可把客服、内容、竞品、线索、数据等搭建为智能体,月成本可低至约 170 元。", "八、AI Agent(智能体)概念与一人公司应用\n- AI 智能体是一套能自主理解目标、制定计划、执行操作、自我修正的完整闭环系统,不同于普通聊天机器人。", "八、AI Agent(智能体)概念与一人公司应用\n- 核心公式:AI Agent = 大模型 + 记忆 + 主动规划 + 工具使用。", "八、AI Agent(智能体)概念与一人公司应用\n- 三大特征:自主决策(自己判断下一步)、工具调用(操作浏览器、调用 API、读写文件、发邮件)、持续学习(优化策略)。", "八、AI Agent(智能体)概念与一人公司应用\n- 成熟智能体通常含五个核心模块:大语言模型(大脑)、任务规划、记忆存储、工具调用、环境感知。", "(如何搭建一个智能体)", "八、AI Agent(智能体)概念与一人公司应用\n- 角色设定:赋予具体人员设置与知识边界,上传历史文档。", "八、AI Agent(智能体)概念与一人公司应用\n- 知识库挂载(RAG):整理笔记、业务 SOP、白皮书,通过文档切片与向量检索让 AI 具备「记忆」。", "八、AI Agent(智能体)概念与一人公司应用\n- 工作流设计:将重复劳动逻辑化,如自媒体运营分身:聚焦热点、生成文案、生成图表、多平台分发。", "八、AI Agent(智能体)概念与一人公司应用\n- 插件与 API 集成:接入搜索、数据库、API 插件,让智能体具备「手脚」。", "八、AI Agent(智能体)概念与一人公司应用\n- 避坑:防止幻觉(强制优先从知识库取答案)、遵循 MVP 原则不过度设计、定期分析对话日志迭代提示词。", "(OPC 的 1+N 组织架构)", "八、AI Agent(智能体)概念与一人公司应用\n- 核心层(1 人)只做四件事:定战略、定标准、审关键输出、管现金流。", "八、AI Agent(智能体)概念与一人公司应用\n- AI 智能体层(N 个数字员工):按岗位拆分为独立 Agent,如产品/研发、内容/营销、流量/投放、销售/客服、交付/项目、财务/行政。", "八、AI Agent(智能体)概念与一人公司应用\n- 外部协作层:把设计、剪辑、翻译等 AI 搞不定的重活外包。", "(流水线工作流与运营闭环)", "八、AI Agent(智能体)概念与一人公司应用\n- 标准工作流:需求输入、AI 拆解、AI 生成初稿、创始人审核微调、AI 优化排版、成品入库或上线。", "八、AI Agent(智能体)概念与一人公司应用\n- 运营闭环:AI 批量产内容并多平台分发、AI 客服 7×24 响应并自动生成报价单、标准化 SOP 加 AI 自动交付、AI 记账报税。", "八、AI Agent(智能体)概念与一人公司应用\n- 效果:启动成本可从传统创业的 20 万-100 万压缩到每月 500-5000 元;试错周期从半年缩短到 2-6 周。", "九、国内主要大模型厂商与选型\n- 国内大模型市场呈「三超多强」格局:第一梯队为豆包(字节)、通义千问(阿里)、DeepSeek;第二梯队为智谱 AI、文心一言(百度)、混元(腾讯);第三梯队为 Kimi、MiniMax 等创新企业。", "九、国内主要大模型厂商与选型\n- 在简单问答、常识领域各大模型差异甚小,差异主要体现在复杂指令、多模态理解生成与多轮对话。", "九、国内主要大模型厂商与选型\n- DeepSeek:推理、代码、数学、性价比强,开源(MIT),API 定价低,适合写代码与高性价比推理。", "九、国内主要大模型厂商与选型\n- 通义千问(阿里):通用、中文、企业云生态强,阿里云百炼 MaaS,长上下文,回答完整度突出。", "九、国内主要大模型厂商与选型\n- 豆包(字节):C 端应用、多模态、Agent,日活高、免费多模态,深度逻辑与代码能力相对弱。", "九、国内主要大模型厂商与选型\n- 智谱 GLM:编程、长程 Agent 任务强,开源、长上下文,政企私有化根基扎实。", "九、国内主要大模型厂商与选型\n- 文心一言(百度):全模态、搜索、中文写作,功能完善度领先、合规资质最全。", "九、国内主要大模型厂商与选型\n- Kimi(月之暗面):超长文档、研报解析领先。", "九、国内主要大模型厂商与选型\n- 讯飞星火(科大讯飞):中文、语音交互、教育医疗,央国企与信创环境应用广。", "九、国内主要大模型厂商与选型\n- 混元(腾讯):社交、多模态、快慢思考,微信生态深度整合。", "九、国内主要大模型厂商与选型\n- 写代码商用:智谱 GLM 或 DeepSeek;长文档处理:Kimi;多模态/免费 C 端:豆包;企业政企落地:智谱、文心一言或通义千问。", "九、国内主要大模型厂商与选型\n- 各家价格口径不统一(有美元/人民币计、缓存命中与上下文位影响单价),比价务必回官网核对同一口径。", "九、国内主要大模型厂商与选型\n- 首期成效:受理 58 个项目,23 个成功入驻,覆盖人工智能、低空经济、跨境服务等领域。", "> 数据口径声明:本知识库内容全部依据《昆明市创业园宣传册》《昆明市大学生创业园入驻企业信息表》《昆明市大学生创业园运行情况统计表(2026年7月)》《附件1.省级创业孵化载体相关情况简介材料》《昆明市大学生创业园创业企业(项目)入驻管理规程》等标准资料整理;涉及运营数据时严格区分「官方统计口径」「省级绩效累计口径」「AI 平台运行指标」三类口径,不得混用。", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 全称:昆明市大学生创业园(KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK)", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 性质:云南省首家由政府主办、免费为创业青年大学生提供创业孵化服务的开放式园区", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 成立时间:2009 年(2009 年 9 月成立)", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 主管部门:昆明市人力资源和社会保障局", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 园区地址:昆明市民航路 229 号(昆明市人力资源中心 1—2 楼)", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 建筑面积:约 3000 平方米,可容纳约 50 个大学生创业团队入驻", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 主导产业:数字经济与电子信息产业", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 认定省级创业园时间:2012 年", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 咨询电话:18087174891 / 17787129131 / 18214573650;接收邮箱:714066050@qq.com", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 服务模式:以「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体全链条创业扶持体系,打造「一站式」创业孵化服务平台", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 实际入驻企业数:38 家(2026 年 7 月报送当月数)", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 园区可容纳企业(实体)个数:49 个", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 开园以来累计投入园区运营资金:560 万元", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计带动就业人数(当年累计):213 人", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计生产经营总额(当年累计):252.15 万元", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计上缴税利总额(当年累计):1.03 万元", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 开园以来累计成功孵化出园企业数:149 家", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 当年开展活动场次:11 场;当年培训人数:362 人", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 备注:统计表 3、9 栏为开园以来累计数,5 栏为报送当月实际入驻数,其余为当年累计数", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 认定为省级创业孵化载体以来累计孵化企业:181 家;当前实有在孵创业实体:31 家", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 团队(实体)出园数:累计 150 家(本年度 11 家)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队发明专利数:累计 171 项(本年度 13 项)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队累计经营收入:21,911.14 万元(约 2.19 亿元)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队累计税收缴纳:426.2 万元", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 累计带动就业:1,529 人(本年度 106 人)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 创业指导专家团队:50 人;运营团队:3 人", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 开展创业指导服务活动:累计 83 场(本年度 18 场)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 近 3 年入孵实体孵化成功率:95.91%", "四、入驻对象与条件(依据《入驻管理规程》)\n- 入驻对象范围:在校大学生及毕业 8 年内的高校毕业生、归国留学生、外籍留学生等青年创业企业(项目);申请人须担任企业法定代表人或项目负责人;对符合昆明市产业发展的优质创业企业(项目)或 OPC 企业可适当放宽入驻条件", "四、入驻对象与条件(依据《入驻管理规程》)\n- 产业方向:符合国家产业政策,聚焦昆明市「10+N」产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等)", "四、入驻对象与条件(依据《入驻管理规程》)\n- 其他要求:有明确可行的经营计划、启动资金和风险承担能力,具备规范的企业管理制度;无知识产权纠纷、无违法犯罪记录、无经营异常记录", "四、入驻对象与条件(依据《入驻管理规程》)\n- 优先入驻情形:获得市级及以上创业大赛奖项、拥有突出学术成就、重大发明创造及高新专利技术的项目", "园区原则上每年组织两次入驻工作,可适时增减调整。流程如下:", "五、入驻流程(依据《入驻管理规程》)\n1. 提出申请:提交《入驻申请表》《创业计划书》、创业者身份证(外籍提供护照、签证)、学历证明、企业营业执照(已注册企业)、相关资质专利证书等材料", "五、入驻流程(依据《入驻管理规程》)\n2. 项目初审:服务机构对申请材料初审,材料不齐的一次性告知补全", "五、入驻流程(依据《入驻管理规程》)\n3. 入驻评审:从《昆明市就业创业导师库》抽取 3 名评审专家,采取路演答辩方式评审,实行量化打分制(总分 100 分,80 分及以上准予通过)。评分维度:团队资质与能力(20 分)、项目创新性与技术壁垒(25 分)、产业适配与市场前景(25 分)、运营规划与落地能力(15 分)、社会价值与发展潜力(15 分)", "五、入驻流程(依据《入驻管理规程》)\n4. 评审公示:报主管部门备案后,在昆明市人力资源和社会保障局官方网站公示,公示无异议的准予入驻", "五、入驻流程(依据《入驻管理规程》)\n5. 签署协议:签订《入驻孵化协议》,分配办公场地/工位,正式入驻", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- OPC 创业空间:面向以人工智能技术为支撑、聚焦数字经济/AI 应用/软件开发/文创设计/电商直播等轻资产领域的项目,提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 创业苗圃区:面向创意阶段、未注册企业的大学生创业团队,提供开放式共享工位,孵化周期最长 2 年;成长好的项目经评审可推荐至孵化加速区孵化 1 年", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 孵化加速区:面向已完成工商注册、处于初创期的企业,提供独立办公空间,基础孵化周期 2 年,期满经评估合格可申请延期 1 年,累计不超过 3 年", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 国际创客区:面向归国留学生、外籍留学生创业项目,提供个性化办公空间及国际化配套服务,孵化周期参照孵化加速区执行", "七、政策与孵化服务(依据《宣传册》)\n- 空间保障:免费提供办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等", "七、政策与孵化服务(依据《宣传册》)\n- 孵化服务:创业指导专家一对一辅导,定期举办创业沙龙、创业讲堂、项目路演等", "七、政策与孵化服务(依据《宣传册》)\n- 融资支持:联动省内外创业投资机构,提供政策性贷款、商业银行贷款、股权融资等推荐指导", "七、政策与孵化服务(依据《宣传册》)\n- 政策对接:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策", "七、政策与孵化服务(依据《宣传册》)\n- 资源整合:对接驻昆高校、科研院所、龙头企业,纳入昆明创业创新服务体系,优先参与「春城创业荟」等品牌活动", "七、政策与孵化服务(依据《宣传册》)\n- 专项赋能培训:定期开展 AI 公益性培训(AI 技术应用、数字内容创作、一人公司创业落地、企业 AI 降本增效)", "七、政策与孵化服务(依据《宣传册》)\n- 其他服务:工商注册、税务代办、知识产权咨询、法律援助等", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 年度考核:量化打分制(总分 100 分),维度为经营发展能力(20)、技术创新能力(25)、合规与履约情况(20)、创业带动成效(20)、发展潜力与孵化成效(15);等级:优秀(80 分及以上)、合格(60—79 分)、不合格(60 分以下)", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 退出类型:成功孵化出园、孵化期满退出、主动申请退出、强制勒令退出", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 强制退出情形:累计 30 个工作日无人到现场办公、连续 7 个工作日场地空置且未报备累计超 3 次、给园区造成重大经济损失或名誉损失、不服从管理情节严重、被吊销执照或破产清算、违法违规经营、私自转租或虚报数据等", "九、OPC 概念(一人公司创业模式)\n- OPC 即 One Person Company「一人公司」创业模式:以人工智能技术为支撑,聚焦数字经济、AI 应用、软件开发、文创设计、电商直播等轻资产领域,一人即可启动运营的创业形态", "九、OPC 概念(一人公司创业模式)\n- 园区 OPC 创业空间为 OPC 创业者提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年;园区定期开展「AI 一人公司实战课」等专项培训", "九、OPC 概念(一人公司创业模式)\n- 云南首个人工智能 OPC 创新人才基地落地昆明(2026 年 3 月公开报道),填补省内个体 AI 创业培育空白", "在园企业以《2026年7月运行情况统计表》官方口径 38 家为准;《入驻企业信息表》收录本期入驻企业(项目)名录 39 家,含云南派音人工智能科技、米勒克尔蓝宝石珠宝产业化、中泰研学合作、云南宸中低空经济、昆明智海银高文化科技、人工智能机器人大模型训练、云南廷秀文旅康养、瀚颖 AI+ 教育咨询、云南大学 AI+ 创业平台、仰光客厅、云南上古绝学文化、千楣之路、中越生物医疗、中越文旅交流、绿态环保科技、联翼航空低空经济、酷享野农 AI 农业、滇缅国际设计、昆明舒诺生物科技、", "达岸教育管理、昆明滇油石化、花仙子园艺肥料、Facebook 越南茶器跨境、研X 同行者网络、鬼才明 AI 创意工作室、朵哈·玫瑰特色产业链、昆明云韵体育、昆明舒护安养老服务、南菌优培食用菌、五华区丽裳文化、云南星瑞航空、云南直播私域平台、昆明屿澈电商、中外青少年研学、星杭商务信息咨询、蓝智科技、云品出滇·纸享万家、云迹轻旅、启元人工智能科技。行业分布(按简介分类):人工智能 7 家、跨境电商 5 家、教育培训 4 家、文旅康养 4 家、文化创意 4 家、生物医药 4 家、", "低空经济 3 家、现代农业 3 家、绿色环保 2 家、企业服务 2 家、新材料 1 家", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- TOKEN 消耗:月均约 200 亿 tokens(折合日均约 6.67 亿 tokens、实时速率约 7,700 t/s)——AI 平台运行锚点测算", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- 工具调用:按平均每次工具调用约 15 万 tokens 折算,与 TOKEN 消耗实时联动", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- 实时运营数据请调用 get_park_overview 工具获取,以工具返回为准", "常见问题\n- 问:如何入驻园区?答:提交申请(入驻申请表、创业计划书、身份证、学历证明等)→ 项目初审 → 入驻评审(导师库抽 3 名专家、路演答辩、80 分通过)→ 评审公示(市人社局官网)→ 签订《入驻孵化协议》、安排场地工位;每年原则上两次。", "常见问题\n- 问:园区有哪些政策扶持?答:空间免费、孵化辅导、融资对接、政策申报、资源整合、AI 专项培训、工商税务法律一站式服务(详见「政策与孵化服务」)。", "常见问题\n- 问:园区有多少家企业、带动多少就业?答:按官方统计(2026 年 7 月)实际入驻 38 家、当年累计带动就业 213 人;省级绩效累计口径(截至 2026 年 5 月)累计带动就业 1,529 人、累计经营收入约 2.19 亿元。请说明所需口径。", "常见问题\n- 问:什么是 OPC?答:一人公司(One Person Company)创业模式,详见「OPC 概念」。", "> 整理自 `数据资料/` 目录全部原始材料:\n> 附件1.省级创业孵化载体相关情况简介材料.xlsx · 昆明市大学生创业园入驻企业信息表.xlsx · 昆明市大学生创业园运行情况统计表(2026年7月).xls · 昆明市大学生创业园创业企业(项目)入驻管理规程.wps · 昆明市创业园宣传册.pdf · 空间布局.png / 详细的布局.png · 昆明市大学生创业园 定稿(品牌设计稿)\n>\n> 本文件尽量保留原始表格、文件中的真实数值与原文字,供对外申报、总结汇报、", "宣传推广等使用。\n---", "目录\n1. [园区基本信息](#1-园区基本信息)", "目录\n2. [运营绩效核心指标(省级申报口径)](#2-运营绩效核心指标省级申报口径)", "目录\n3. [2026年1—7月运行情况月统计](#3-2026年17月运行情况月统计)", "目录\n4. [入驻企业完整名录(39家/信息表)](#4-入驻企业完整名录39家信息表)", "目录\n- [A. 孵化加速区(加速1–10)](#a-孵化加速区)", "目录\n- [B. 国际创客区(国际1–7)](#b-国际创客区)", "目录\n- [C. 成长区(成长1–8)](#c-成长区)", "目录\n- [D. OPC创业空间](#d-opc创业空间)", "目录\n- [E. 一楼直播间](#e-一楼直播间)", "目录\n5. [省级申报·入驻实体名单(31家)](#5-省级申报入驻实体名单31家)", "目录\n6. [企业(项目)入驻管理规程要点](#6-企业项目入驻管理规程要点)", "目录\n7. [空间布局](#7-空间布局)", "目录\n8. [宣传册核心内容(园区简介·服务·政策)](#8-宣传册核心内容园区简介服务政策)", "目录\n9. [入驻条件与入驻流程](#9-入驻条件与入驻流程)", "目录\n10. [孵化周期(四大孵化区域)](#10-孵化周期四大孵化区域)", "目录\n11. [报名方式](#11-报名方式)", "目录\n12. [媒体报道与合作媒体](#12-媒体报道与合作媒体)", "目录\n13. [园区新闻与活动大事记](#13-园区新闻与活动大事记)", "目录\n14. [品牌VI设计规范](#14-品牌vi设计规范)", "| 项目 | 内容 |\n|---|---|\n| 载体名称 | 昆明市大学生创业园 |\n| 载体类别 | 省级创业园(2012年获评) |\n| 载体地址 | 云南省昆明市官渡区民航路229号(昆明市人力资源中心二楼) |\n| 成立时间 | 2009年9月(2026年4月全面优化后“焕新亮相”) |\n| 主管部门 | 昆明市人力资源和社会保障局 |\n| 运营机构 | 云南汇浙青创园区投资有限公司(统一社会信用代码 91530112MA6PWKTUX2) |\n| 申报单位 | 昆明市人力资源和社会保障局 |\n| 申报单位负责人 | 黄斌(联系电话 63362979) |\n| 运营负责人 | 李云鹏(联系电话 18087147411) |\n| 载体特色 | 云南省首家政府主办、", "为创业大学生免费提供创业平台的开放式园区 |\n| 主导产业 | 数字经济与电子信息产业 |\n| 载体占地面积 | 3000 平方米 |\n| 孵化场所面积 | 800 平方米 |\n| 载体资产总值 | 400 万元 |\n| 总投资(开园) | 近 100 万元 |\n| 累计投入园区运营资金 | 560 万元 |\n| 可容纳团队(实体)户数 | 50 户(统计表中某口径为 49 个) |\n| 园区等级/称号 | 国家大学科技园体系、省级创业园等(宣传册口径:国家众创空间、", "国家级科技企业孵化器等称号口径不一,以正式文件为准) |\n**园区简介(300字内口径)**\n昆明市大学生创业园位于昆明市民航路301号昆明人力资源中心二楼,2009年9月成立,总投资近100万元,是云南省首家免费为创业大学生提供创业平台的开放式园区,隶属于昆明市人力资源和社会保障局,由昆明市大学生创业园服务中心承担日常管理服务。园区总面积约3000平方米,可容纳50个大学生创业团队入驻,为入驻团队提供免费办公场地及配套设施,并配套政策咨询、创业规划、创业指导、创业培训、", "导师对接、跟踪服务、资金扶持、法律援助等全维度创业支持服务。园区坚持“高起点、高标准、高水平”办园宗旨,遵循“优质、高效、便捷”服务原则与“规范、完善、提高”管理理念,致力打造企业家培育摇篮与全省示范创业园区。截至2026年5月,园区累计孵化企业181家,其中在孵31家、成功出园149家,累计带动就业1529人、上缴利税426.2万元,38家企业累计获市级创业扶持资金149.25万元。\n---", "> 出处:附件1.2 省级创业孵化载体简介(运营绩效情况)\n| 指标 | 认定以来累计 | 本年度 | 当前实有 |\n|---|---|---|---|\n| 可容纳团队(实体)户数 | — | 50 户 | — |\n| 已入驻团队(实体)户数 | 181 户 | 31 户 | 31 户 |\n| 其中:应届高校毕业生(含离校2年内)参创实体数 | 114 户 | 6 户 | — |\n| 团队(实体)出园数 | 150 户 | 11 户 | — |\n| 返乡农民工参加创办实体数 | 0 户 | 0 户 | — |\n| 入驻团队(实体)发明专利数 | 171 项 | 13 项 | — |\n| 入驻团队(实体)经营收入 | 21911.14 万元 | 161.3 万元 | — |\n| 入驻团队(实体)税收缴纳金额 | 426.2 万元 | 0.22 万元 | — |\n| 入驻团队(实体)获得创业资金扶持金额 | 38 户 | 149.25 万元 | — |\n| 孵化创业实体数 | 181 户 | — | 31 户(在孵) |\n| 带动就业人数 | 1529 人 | 106 人 | — |\n| 运营团队人数 | — | 3 人 | — |\n| 创业指导专家团队人数 | — | 50 人 | — |\n| 开展创业指导服务活动次数 | 83 场 | 18 场 | — |\n| 年末孵化场所利用率 | 90%(0.9) | — | — |\n| 近3年入孵实体孵化成功率 | 95.91% | — | — |\n| 近3年入孵实体到期出园率 | 100%(1) | — | — |\n---", "> 出处:昆明市大学生创业园运行情况统计表(2026年7月).xls —《昆明市创业平台运行情况月统计表》及《年度汇总表》", "### 3.1 2026年7月当月快照\n| 指标 | 数值 |\n|---|---|\n| 园区建筑面积 | 3000 m² |\n| 开园以来累计投入园区运营资金 | 560 万元 |\n| 园区可容纳企业(实体)个数 | 49 个 |\n| 园区实际入驻企业数 | 38 个 |\n| 入驻企业累计带动就业人数 | 213 人 |\n| 入驻企业累计生产经营总额 | 252.15 万元 |\n| 入驻企业累计上缴税利总额 | 1.03 万元 |\n| 开园以来累计成功孵化出园企业(实体)数 | 149 户 |\n| 开展活动场次 | 11 场 |\n| 培训人数 | 362 人 |\n> 备注:报表第3、", "9栏数据为开园以来累计数,第5栏为报送当月实际入驻企业数,其他栏均为当年累计数。", "### 3.2 2026年1—7月逐月变化(年度汇总表)\n| 月份 | 园区建筑面积(m²) | 累计运营资金(万元) | 可容纳个数 | 实际入驻企业数 | 带动就业(人) | 累计生产经营总额(万元) | 累计上缴税利(万元) | 累计出园(户) | 活动场次 | 培训人数 |\n|---|---|---|---|---|---|---|---|---|---|---|\n| 1月 | 700 | 510 | 18 | 15 | 73 | 28.3 | 0.04 | 139 | 0 | 0 |\n| 2月 | 700 | 510 | 18 | 15 | 73 | 50.4 | 0.08 | 139 | 0 | 0 |\n| 3月 | 700 | 560 | 18 | 5 | 73 | 59.1 | 0.09 | 149 | 0 | 0 |\n| 4月 | 3000 | 560 | 49 | 24 | 175 | 69.73 | 0.10 | 149 | 0 | 0 |\n| 5月 | 3000 | 560 | 49 | 31 | 179 | 161.3 | 0.22 | 149 | 5 | 149 |\n| 6月 | 3000 | 560 | 49 | 31 | 186 | 170.4 | 0.35 | 149 | 7 | 206 |\n| 7月 | 3000 | 560 | 49 | 38 | 213 | 252.15 | 1.03 | 149 | 11 | 362 |\n> 数据说明:4月起园区迁入民航路229号新场地,建筑面积由700㎡增至3000㎡、", "可容纳数由18个增至49个,运营资金由510万元增至560万元。截至7月底实际入驻38个,累计带动就业213人。\n**填报信息**:单位负责人 李云鹏 / 填表人 李禹贤 / 联系电话 18087174891 / 邮箱 714066050@qq.com / 填表时间 2026年7月31日\n---", "> 出处:昆明市大学生创业园入驻企业信息表.xlsx(共39个项目,序号1–39,按“场地安排”列分区分布)\n> 分区口径:加速=孵化加速区、国际=国际创客区、成长=成长区、OPC=OPC创业空间、一楼直播间。", "### A. 孵化加速区\n(加速1—加速8、加速10,共9家;表中无“加速9”)", "**加速1|云南派音人工智能科技有限公司**\n- 负责人:王思松,2020年毕业于应急管理大学新闻学专业。\n- 简介:OPC一人公司,2026年3月启动,入驻不足一个月即获北京相关投资支持,2026年4月正式注册成立,吸纳就业4人。核心业务主攻人工智能算法及音频模型应用研究,兼备内容创作、运营管理与技术研发综合能力,专注多模态音频技术研发。自研Pine全系列模型家族,覆盖音频识别、向量嵌入、音效生成、配乐创作及数字内容创意中枢,贯通音频理解至声音合成全链路智能。", "**加速2|米勒克尔蓝宝石珠宝产业化(项目)**\n- 负责人:王昱博,2023年3月毕业于美国加州大学圣克鲁兹分校 Multi-/Interdisciplinary 管理学专业。\n- 简介:2026年3月启动,海归留学生发起,覆盖米勒克尔大尺寸蓝宝石新材料与珠宝产业化全链条(晶体生长、材料精密加工、终端珠宝设计)。与产业链上下游深度协同,专注高品质、高附加值蓝宝石珠宝产品,推动蓝宝石新材料与珠宝产业深度融合。", "**加速3|中泰研学合作(项目)**\n- 负责人:刘泽位,泰国西那瓦大学工商管理博士二年级,昆明市侨联青年委员会副会长,精通英语、熟练泰语。\n- 简介:2026年启动运营,海归在读博士发起,围绕西那瓦大学文理学院管理运营,统筹泰国环球工商职业学院中国区事务。与中泰院校、机构深度合作,提供出国留学、中外合作办学、校企合作、产教融合及企业出海一站式服务。", "**加速4|云南宸中低空经济有限公司**\n- 负责人:王爱玲,云南大学EMBA在读,公司负责人。\n- 简介:2026年2月成立,扎根园区的创新型初创企业,专注低空经济全产业链开发运营,依托园区创业扶持体系与营商环境,快速完成项目落地与团队组建,致力于成为云南低空经济领域新兴力量。", "**加速5|昆明智海银高文化科技有限公司**\n- 负责人:宋芃燃,昆明文理学院审计学专业在读,中共党员,公司合伙人。\n- 简介:2026年4月成立,聚焦数字化财税与企业咨询服务,覆盖传统财税、AI智能财税、海外财税、财务人才培训及企业综合咨询。依托AIGC技术打造AI集成服务平台,提供财税咨询、代理记账、法律合规、企业管理、出海服务等一站式解决方案。", "**加速6|人工智能机器人大模型训练(项目)**\n- 负责人:周凯,2022年毕业于天津大学机械设计及其自动化专业。\n- 简介:2026年4月成立,聚焦人工智能机器人大模型训练,围绕大模型研发与落地提供全流程数据支撑与技术服务,开展数据采集、清洗、标注、模型训练及优化迭代,推动机器人大模型在多场景落地应用。", "**加速7|云南廷秀文旅康养服务(项目)**\n- 负责人:李雨桐,2025年毕业于日本樱美林大学老年社会学硕士。\n- 简介:负责人计划5月归国创业。立足云南“旅居云南”趋势与银发经济增长需求,解决民宿、旅居空间、康养点位在服务标准、适老能力、长期居住支持和运营转化方面的短板,建立面向银发人群的文旅旅居服务支持体系。", "**加速8|瀚颖AI+教育信息咨询(项目)**\n- 负责人:王翰啸,2019年毕业于云南大学滇池学院财务管理专业。\n- 简介:昆明瀚颖“AI+数字教育”项目2026年4月成立,由创业培训服务领域人员发起,融合AI与教育教学体系,打造集课程研发、人才培育、企业赋能、师资共建于一体的全链条数字化教育服务生态。", "**加速10|云南大学AI+联合创业服务平台(项目)**\n- 负责人:顾耘华,云南大学艺术与设计学院艺术设计在读硕士。\n- 简介:云南大学在校研究生、毕业生和创业导师共同发起的AI创业平台。以OPC社区AI人才培养与就业服务为核心,覆盖AI短视频、AI漫剧、AI+PPT制作美化等。已帮助多名大学生成功开展OPC创业;联合云南AIGC产业联盟打造“育笋计划”公益品牌,辐射大学生就创业500人+;王升雅《生态秘境》插画在《人与自然》杂志连载六期;王洋《智绘古海——AI复原寒武纪古生物》获国家级创赛金奖;杨沐晨打造西南城市鸟类多样性生态视觉品牌,承办7项生态展览;王润曦协同云南省委宣传部助力会泽白雾村打造乡村文旅振兴名片。", "### B. 国际创客区\n(国际1—国际7,共7家)", "**国际1|仰光客厅(项目)**\n- 负责人:缅甸籍留学生,毕业于云南大学经济学院。\n- 简介:缅甸留华学生发起的中缅文旅融合项目,以仰光近郊沉浸式文化营地为核心载体,面向中国客群、在缅中资机构及本地青年,集住宿、特色餐饮、文化体验、城市导览于一体,与本地社区、手工艺人合作带动就业与增收,搭建跨文化交流平台。", "**国际2|千楣之路(项目)**\n- 负责人:缅甸籍留学生,现就读于云南大学工商管理学院。\n- 简介:缅甸籍留学生创立,主营缅甸传承两千余年的传统护肤产品檀娜卡(黄香楝树干制成,缅甸已于2025年申报联合国非遗)。打造传统原木套装、便捷粉泥、现代美妆三大产品线,以“文化内容+跨境电商”模式引入中国市场。", "**国际3|联翼航空技术低空经济应用国际化拓展(项目)**\n- 负责人:杨俊蓝,2023年毕业于云南财经大学电子商务专业,瑞士商学院MSC硕士在读。\n- 简介:2026年4月启动,在读硕士发起,立足云南面向东南亚的区位门户优势,开展低空经济业务国际化拓展,在航空通航、人工智能与数字信息、教育培训方向布局,输出无人机应用、低空数据服务、跨境技能培训等内容,推动低空经济产业、人才、服务双向辐射东南亚。", "**国际4|酷享野农AI农业(项目)**\n- 负责人:晏和波,西南林业大学农业工程与信息技术专业在校研究生。\n- 简介:2026年4月启动,西南林业大学在校研究生发起,聚焦智慧农业、科研种植、农旅融合,用AI智能视觉提供农业宣传内容制作服务。与科研机构合作开展高原早熟苹果科研种植,与地方政府共建共享农庄,助力乡村振兴。", "**国际5|滇缅国际设计(项目)**\n- 负责人:张浩鹏,2022年毕业于云南财经职业学院审计学专业。\n- 简介:2026年4月启动,依托一带一路机遇,立足滇缅对外开放节点开展工程设计服务,创新跨国协同设计模式,融入绿色建造技术,覆盖交通、城乡基建等领域,与境外机构合作推动跨境项目落地。", "**国际6|昆明舒诺生物科技有限责任公司**\n- 负责人:舒岐涛,云南林业职业技术学院环境艺术专业在校生。\n- 简介:2026年4月成立,聚焦生物科技与宠物营养领域,围绕精准营养、全生命周期养护、清洁标签开展业务,以科学配方与生物活性技术打造宠物精细化营养产品。", "**国际7|达岸教育管理(云南)有限公司**\n- 负责人:吕琳,2022年毕业于泰国暹罗大学MBA研究生专业。\n- 简介:专注德国双元制教育与德国留学一站式服务(2024年启动),与德国院校合作,提供双元制培养、公立院校申请、德语培训、签证办理、境外陪护等全流程服务,帮助学生实现带薪留学、学历提升与稳定就业。", "### C. 成长区\n(成长1—成长8,共8家)", "**成长1|花仙子园艺肥料(云南)有限公司**\n- 负责人:翁淞耀,2023年毕业于云南师范大学社会体育指导与管理专业。\n- 简介:2025年4月成立,专注家庭园艺肥研发生产,针对产品异味大、品质不稳、使用不便等痛点推出安全无异味、易操作系列产品,已实现规模化生产。采用线上线下融合渠道,服务家庭养花、阳台种植民生需求。", "**成长2|研X——同行者网络(项目)**\n- 负责人:陈品祥,2024年昆明理工大学硕士毕业,兼职任教昆明冶金职业大学。\n- 简介:面向全国1621万硕博及6000万泛科研人群的一站式学术成长与资源共享平台。首创“1+6+N”生态(1个实名制平台+文献工具导航、代码共享交易、就业创业对接、学术/情感社交婚恋、科教娱乐、研究生严选商城6大模块)。已完成安卓、iOS、Windows三端开发并上架,已有全国300余所高校注册,两位高校教授以个人资金投入种子轮。", "**成长3|朵哈·玫瑰特色产业链(项目)**\n- 负责人:刘志贤,云南轻纺职业学院艺术设计专业大二在校生。\n- 简介:2026年4月启动,云南轻纺学院在校大学生发起,立足昆明安宁八街核心产区,以地标产品重瓣红玫瑰为核心原料,主打玫瑰米酿特色饮品,与省内农业产业化龙头企业合作实现原料优选、标准化生产与品质管控。", "**成长4|昆明舒护安养老服务有限责任公司**\n- 负责人:严宇佳,2019年毕业于云南艺术学院广播电视编导专业。\n- 简介:OPC一人公司,入驻园区孵化仅一个月成功吸纳就业3人。专注正规医疗护理服务,主营医院专属陪护与居家上门护理,覆盖术后康复、失能半失能照料、慢病护理、偏瘫照护、生活监护等全场景,与医疗机构深度合作。", "**成长5|南菌优培——食用菌专用生物制剂创新与高原特色野生菌高值化全链开发(项目)**\n- 负责人:王荣飞,2026年毕业于中山大学外科学专业。\n- 简介:三年以上研发基础与产业化探索,核心团队含分析化学博士、农业硕士、医学硕士等。计划2026年7月完成公司注册并启动。聚焦食用菌专用生物制剂(引入海藻糖-6-磷酸、寡糖等生物源成分)与高原特色野生菌(硬皮地星、硬皮马勃)高值化全链开发。", "**成长6|五华区丽裳文化艺术工作室**\n- 负责人:王娟,2024年毕业于昆明幼儿师范高等专科学校幼儿教育专业。\n- 简介:2026年4月成立,专注传统文化、民俗活动、茶文化及国风品牌活动策划执行与文旅研学。依托云南自贸区优势,推动民俗文创、茶产品、干花制品及高原特色产品出海,搭建“账号引流+内容传播+产品出海”运营体系。", "**成长7|云南星瑞航空(项目)**\n- 负责人:尹永浩,2019年毕业于滇西科技师范学院亚洲微电影学院数字媒体技术专业。\n- 简介:2026年4月启动,专注低空智能应用,以大疆行业级无人机为核心装备,业务涵盖无人机测绘、三维建模、数字孪生、应急救援、公共安全巡检、夜间红外搜救及电力、光伏、油气管道、森林防火等作业,并深耕文物数字化保护。具备硬件销售、数据处理、技术支持、人才培训一体化能力。", "**成长8|综合性云南直播私域服务平台**\n- 负责人:金满满,2026年7月毕业于周口师范学院学前教育专业。\n- 简介:综合性云南直播私域服务平台,深耕云南本土特色资源,打造自主IP形象,以短视频创作+全域直播运营为核心,搭建全国性自主私域流量池;同时搭建大学生创业孵化通道,构建人才迭代体系。", "### D. OPC创业空间\n(场地安排=OPC,共6家)", "**D1|中外青少年研学(项目)**(OPC)\n- 负责人:王得荣,2022年武汉大学法学专业毕业,2024—2025年英国剑桥大学短期交流学习。\n- 简介:国际游学与教育交流服务项目,已与美国、英国、澳大利亚、新加坡等多国顶尖教育机构达成长期直营战略合作,为青少年、在职教师及家长提供一站式全球化教育交流解决方案。", "**D2|星杭商务信息咨询昆明有限责任公司**\n- 负责人:杨之华,2024年6月毕业于云南师范大学教育学专业。\n- 简介:主要经营商务服务、信息咨询培训、商务接待及服务保障配套。", "**D3|蓝智科技(项目)**\n- 负责人:徐安厅,2026年毕业于昆明城市学院计算机科学与技术专业。\n- 简介:OPC一人公司,2026年5月启动,从业3人。立足西南蓝莓特色种植产业,专注农业物联网技术研发与智慧基地数字化运营,自研蓝智全系列智慧种植监控系统,涵盖田间环境监测、基质养分检测、水肥灌溉、种植生产数据管理等。", "**D4|云品出滇·纸享万家(项目)**\n- 负责人:王帅,2024年毕业于昆明理工大学电子信息工程专业。\n- 简介:2026年3月成立,以新型悬挂式抽纸为核心切入产品,依托AI工具与电商平台,打造国内电商+跨境电商双增长模式的日用纸品品牌。当前已在拼多多平台实现真实销售验证,未来向东南亚跨境电商扩展。", "**D5|云迹轻旅——昆明本土一站式轻量化文旅服务工作室(项目)**\n- 负责人:王海璇,云南大学工商管理(MBA)毕业,10年云南文旅从业经验。\n- 简介:单人OPC轻资产运营,构建“内容+服务+技术”三位一体业务矩阵:文旅自媒体IP孵化、私人小众旅行定制、非遗自然研学小团、文旅商业咨询策划、AI数字文旅技术服务五大板块。面向C端游客与B端本地文旅商家提供服务。", "**D6|启元人工智能科技(项目)**\n- 负责人:李瑞康,2017年毕业于吉林动画学院广播电视编导专业。\n- 简介:OPC一人公司,专注8-12岁儿童人工智能工具的技术推广与科普体验服务,自主研发适配低龄儿童的AI实操工具包,依托共享空间开展少儿AI创意科技体验沙龙(AI绘画、AI音乐创作、AI视频制作)。", "> 其他在简介中表述为“OPC一人公司”但按场地安排归入其他分区的项目:云南派音人工智能科技(加速1)、昆明舒护安养老服务(成长4)、云南大学AI+联合创业服务平台(加速10)等。\n> 另:云南大学AI+联合创业服务平台完整介绍见【孵化加速区-加速10】。", "### E. 一楼直播间\n(共1家)", "**一楼直播间|昆明屿澈电子商务有限公司**\n- 负责人:杨君武,2024年6月毕业于云南交通职业技术学院建设工程管理专业。\n- 简介:高校毕业生发起的东南亚跨境电商AI智能直播+短视频出海项目,依托TikTok Shop正规跨境渠道,面向马来西亚、泰国、越南、菲律宾四国,主营日用百货、家居用品、电子配件、文创收纳、便携小家电等。采用AI数字人7×24小时无人直播、AI短视频批量引流、1688一件代发轻资产模式。", "### F. 信息表中“场地安排”列为空的其他项目\n《入驻企业信息表》中以下项目(共8家)的“场地安排”列未标注分区,照原文列出供全量归档:\n**云南上古绝学文化发展有限公司(简称:上古书院)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:赵泳淇,2024年毕业于贵州大学教育技术学专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:专注上古家学文化传承与国学赋能的文化机构,以“修身齐家”为核心理念,业务涵盖家学文化课程体系研发、国学经典文化交流、家训家规个性化定制及文化研学,走“从文化体验到家族传承”路径。", "**中越生物医疗(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:武妙英(VŨ DIỆU ANH),越南籍留学生,云南大学工商管理专业在读,精通越语、法语、中文、英语。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:聚焦越南标准化生物蛋白生产联合实验室建设,计划覆盖区域80%以上缺口市场,依托中越技术协同与资源互补开展高纯度生物活性蛋白合规化跨境生产与出口,满足疫苗、诊断试剂及医疗器械领域需求。已获越南五部门联合认证。", "**中越文旅交流(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:阮胡竹珠,云南民族大学广播电视专业在读(2027年毕业)。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:搭建中越文化旅游交流平台,提供旅游咨询、行程定制、文化体验、语言服务及跨境旅游推广,依托广播电视专业背景与中越双语优势开展中越旅游宣传内容制作、新媒体运营、跨境文化交流活动策划。", "**“绿态”环保科技项目**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:陈国旗,云南民族大学在读留学生,负责海外工厂对接、中越跨境供应链统筹及中国全渠道分销网络核心运营。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:紧抓“禁塑令”及绿色环保政策导向,专注天然槟榔叶(mo cau)环保餐具研发、跨境贸易与新媒体全渠道销售。在越南本土自有实体生产工厂,100%越南本土完成从源头集采到杀菌包装,成品经正规清关出口至中国,在昆明园区设“中国市场总分销库与营销/直播工作室”,构建“抖音直播+货架电商双驱闭环”。", "**昆明滇油石化有限责任公司**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:李政德,预计2027年毕业于云南国土资源职业学院地球物理勘探专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:专注生物基纳米驱油剂的研发、生产与销售,为国内低渗透油藏及老油田二次/三次采油提供绿色高效增产解决方案。目标用户:中石油(华北、长庆、新疆、大庆等)、中石化(胜利、中原等)、中海油(渤海等)下属老油田;延伸至一带一路沿线国家老油田(中亚、中东、非洲)及民营油服企业(如延长石油)。", "**Facebook越南市场普洱茶与建水紫陶壶跨境电商(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:罗有艳,2020年毕业于云南农业大学越南语专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:依托Facebook全域流量生态(主页、直播、短视频、广告),聚焦越南垂直市场,主营普洱茶、手工茶具(云南建水紫陶小品壶及公道杯组合)、中式茶礼套装跨境零售。借助“内容种草+直播转化+9610跨境报关+越南专线直邮”闭环模式。", "**鬼才明AI创意工作室(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:赵广明,2022年毕业于重庆大学计算机专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:2026年4月启动,聚焦产教融合、创新创业赛事运营与数智化品牌赋能,围绕AI技术商业化落地、新商科专业规划、全品类活动策展及职业技能人才培育开展服务。搭建AI智能交互体系与AI数字人内容输出矩阵,为政校企提供AI场景应用、全域获客、职业技能培训、赛事集训、资格认证、品牌全案、项目申报等全链路解决方案。", "**昆明云韵体育有限责任公司 / 男子自行车洲际队(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:舒江贵,2020年7月毕业于麒麟区职业学校运动训练专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:2026年4月发起,拟在云南注册成立UCI男子自行车洲际队,打造立足云南、辐射全国、征战亚洲赛场的职业自行车竞技项目,依托云南高原训练优势,构建“职业赛事+商业赞助+高原训练+体育文旅+青苗人才培养”一体化运营模式。", "> 注:申报名单(第5节)将该“男子自行车洲际队项目”归类为 OPC。\n---", "> 出处:附件1.3 创业孵化载体入驻实体名单(省级申报口径,31家)\n| 序号 | 入驻经营实体或项目团队名单 |\n|---|---|\n| 1 | 昆明舒护安养老服务有限责任公司 |\n| 2 | 星杭商务信息咨询(昆明)有限责任公司 |\n| 3 | 达岸教育管理(云南)有限公司 |\n| 4 | 五华区丽裳文化艺术工作室 |\n| 5 | 米勒克尔蓝宝石(项目) |\n| 6 | 中越生物合作——高纯度鲎血蛋白提取的跨境技术创新和商业推广(项目) |\n| 7 | 云南宸中低空经济有限公司 |\n| 8 | 中泰教育合作(项目) |\n| 9 | 人工智能机器人训练(项目) |\n| 10 | 欧森纳东盟香港留学(项目) |\n| 11 | 仰光客厅中缅文化体验营地(项目) |\n| 12 | PineSound派音智能——下一代音效与配乐基座(项目) |\n| 13 | 千楣之路——缅甸“千年之木”Thanaka非遗文化传播与跨境贸易(项目) |\n| 14 | 昆明朵哈食品有限公司 |\n| 15 | 滇缅一带一路国际化建设设计(项目) |\n| 16 | 云南定影文化传播有限公司 |\n| 17 | 酷享野农AI农业 |\n| 18 | 昆明智海银高文化科技有限公司 |\n| 19 | 鬼才明AI创意策划工作室(项目) |\n| 20 | 昆明就惯你生物科技(项目) |\n| 21 | 联翼航空技术低空经济应用国际化拓展(项目) |\n| 22 | 中外青少年研学交流(项目) |\n| 23 | 昆明瀚颖教育信息咨询有限公司 |\n| 24 | 花仙子园艺肥料(云南)有限公司 |\n| 25 | 云南星瑞航空科技(项目) |\n| 26 | 男子自行车洲际队项目(OPC) |\n| 27 | 云南廷秀文旅康养服务项目(OPC) |\n| 28 | 中通乐创(项目) |\n| 29 | 跨境电商出口运营项目 |\n| 30 | 华寮国际教育交流(云南)有限责任公司 |\n| 31 | 综合性云南直播私域服务平台 |\n> 说明:本名单项目名称部分与《入驻企业信息表》略有出入(如“米勒克尔蓝宝石”、", "“欧森纳东盟香港留学”、中越生物改名等),系两表不同填报口径/时点所致,均照原文保留。\n---", "> 出处:昆明市大学生创业园创业企业(项目)入驻管理规程.wps(昆明市人力资源和社会保障局,2026年3月13日)", "### 一、实施主体", "6. 企业(项目)入驻管理规程要点\n- 运营主管:昆明市人力资源和社会保障局(主管部门)", "6. 企业(项目)入驻管理规程要点\n- 执行:通过政府购买服务委托第三方服务机构(服务机构),负责入驻全流程工作;主管部门行使监督、考核、验收职权。", "### 二、入驻条件", "6. 企业(项目)入驻管理规程要点\n- **对象范围**:在校大学生及毕业8年内高校毕业生、归国留学生、外籍留学生等青年创业企业(项目),申请人须担任法定代表人/项目负责人。符合昆明市产业发展的优质企业(项目)或OPC企业可适当放宽。", "6. 企业(项目)入驻管理规程要点\n- **产业方向**:聚焦昆明市“10+N”产业链——数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等,具备创新性、可操作性、市场前景良好。", "6. 企业(项目)入驻管理规程要点\n- **其他要求**:明确可行的经营计划、启动资金和风险承担能力,规范的企业管理制度;无知识产权纠纷、违法犯罪记录、经营异常记录;守法经营、诚实守信,遵守园区各项规定。", "6. 企业(项目)入驻管理规程要点\n- **优先入驻**:获得市级及以上创业大赛奖项、突出学术成就、重大发明创造及高新专利技术的企业(项目)。", "### 三、入驻流程", "6. 企业(项目)入驻管理规程要点\n1. **提出申请**:提交《入驻申请表》《创业计划书》、身份证(外籍提供护照/签证)、学历证明、营业执照(已注册)、资质专利证书等。", "6. 企业(项目)入驻管理规程要点\n2. **项目初审**:服务机构初审,材料不齐一次性告知补全。", "6. 企业(项目)入驻管理规程要点\n3. **入驻评审**:服务机构从《昆明市就业创业导师库》抽取3名评审专家,路演答辩评价,量化打分制(100分),80分及以上通过。", "6. 企业(项目)入驻管理规程要点\n- 团队资质与能力 **20分**(学历背景、专业匹配度、创业经验、组织管理、人员稳定)", "6. 企业(项目)入驻管理规程要点\n- 项目创新性与技术壁垒 **25分**(核心技术/商业模式创新点、自主知识产权、技术领先性、行业壁垒)", "6. 企业(项目)入驻管理规程要点\n- 产业适配与市场前景 **25分**(与重点产业链匹配度、市场需求、竞品、市场拓展、盈利模式)", "6. 企业(项目)入驻管理规程要点\n- 运营规划与落地能力 **15分**(商业计划完整性、启动资金、经营管理制度、阶段目标、落地方案)", "6. 企业(项目)入驻管理规程要点\n- 社会价值与发展潜力 **15分**(带动就业能力、税收贡献预期、行业潜力、可持续发展)", "6. 企业(项目)入驻管理规程要点\n4. **评审公示**:报主管部门备案后,在市人社局官网公示,无异议准予入驻。", "6. 企业(项目)入驻管理规程要点\n5. **签署协议**:签订《入驻孵化协议》,分配办公场地/工位,正式入驻。", "### 四、入驻周期(梯度孵化四大区域)\n| 孵化区域 | 面向对象 | 配套 | 孵化周期 |\n|---|---|---|---|\n| OPC创业空间 | 以AI为支撑、聚焦数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域 | 拎包入驻的独立办公位或共享工位 | 最长2年 |\n| 创业苗圃区 | 创意阶段、未注册企业的大学生创业团队 | 开放式共享工位 | 最长2年(落地成长好可推荐转加速区1年) |\n| 孵化加速区 | 已完成工商注册、", "处于初创期的企业(项目) | 独立办公空间 | 基础2年,评估合格可延1年,累计≤3年 |\n| 国际创客区 | 归国留学生、外籍留学生创业企业(项目) | 个性化办公空间+国际化配套服务 | 参照孵化加速区 |", "### 五、项目年度考核", "6. 企业(项目)入驻管理规程要点\n- **考核维度**(量化打分制,100分):经营发展能力(20):营收/利润/纳税、经营计划完成度、管理制度完善度;技术创新能力(25):知识产权新增、研发投入、产品/服务迭代、产学研合作;合规与履约情况(20):遵守管理制度、协议履约、场地规范、材料报送、守法经营;创业带动成效(20):新增就业(高校毕业生占比)、实习实训岗位;发展潜力与孵化成效(15):融资进展、资质认证、市场拓展、可持续规划、出孵成熟度。", "6. 企业(项目)入驻管理规程要点\n- **考核等级**:优秀(80分及以上)、合格(60–79分)、不合格(60分以下)。", "6. 企业(项目)入驻管理规程要点\n- **结果处理**:合格准予继续孵化(期满可申请延期);不合格下达《整改通知书》,整改期满仍不合格的可解除孵化协议、勒令限期退出。", "### 六、退出管理(四种类型)", "6. 企业(项目)入驻管理规程要点\n1. **成功孵化出园**:满足任一成熟出孵标准——①产权明晰,有自主核心产品并规模化;②经营良好,产品有稳定市占率与盈利;③制度健全、组织完备、团队稳定;④已获行业经营资质、质量稳定;⑤有持续研发能力与规划。", "6. 企业(项目)入驻管理规程要点\n2. **孵化期满退出**:周期届满未申请延期或延期未通过、已延期到期。", "6. 企业(项目)入驻管理规程要点\n3. **主动申请退出**:因经营问题、团队变动等申请退出。", "6. 企业(项目)入驻管理规程要点\n4. **强制勒令退出**:①协议签订后累计30个工作日无人到现场办公,或连续7个工作日空置且未报备累计超3次;②违反规定造成重大经济损失/名誉损失;③不服从管理情节严重;④被停业、吊销执照、破产清算;⑤违法经营;⑥私自转租、变更经营项目;⑦虚报数据、伪造材料、拒不配合。", "> 出处:空间布局.png(概念布局)与 详细的布局.png(分区布置图)\n**概念布局(空间布局.png)**:顶部左侧为“玻璃顶”(开敞中庭/天光区);上部右侧以“过道”纵贯,两侧各若干“房间”;中上部为“大厅”;中部横向依次为若干“房间”、橙色的“共享工位区”、绿色的“休息厅”;底部由完整“过道”贯通,下接一排9间“房间”。", "**分区布置(详细的布局.png)**:\n| 区域类别 | 编号/名称 | 数量 |\n|---|---|---|\n| 成长区 | 成长区01–08 | 8 |\n| 国际区 | 国际区01–07 | 7 |\n| 加速区 | 加速区01–13 | 13 |\n| 共享办公 | 共享工位区 | 1 |\n| 会议空间 | 小会议室 / 大会议室 | 2 |\n| 功能空间 | 玻璃顶(中庭)、休息厅、过道、", "楼道 | 若干 |\n> 布局总览:上部左侧为“玻璃顶”开敞区,右上依次为成长区与国际化两纵列;中部靠上为共享工位区与休息厅;下方以“过道”为轴,分布加速区、大小会议室及楼道。分区命名与《入驻企业信息表》中的“加速/国际/成长/OPC”场地安排完全对应。\n---", "> 出处:昆明市创业园宣传册.pdf(16页彩页;以下为正文要点,图片部分略)", "### 8.1 园区定位与理念", "8. 宣传册核心内容(园区简介·服务·政策)\n- 云南省首家由政府部门主办、为创业青年大学生提供**免费创业孵化服务**的**开放式园区**。", "8. 宣传册核心内容(园区简介·服务·政策)\n- 2009年正式成立,昆明市人力资源和社会保障局主办运营,2012年获评省级创业园;位于民航路229号昆明市人力资源中心1–2楼,总面积约3000平方米。", "8. 宣传册核心内容(园区简介·服务·政策)\n- 以助力青年大学生创业梦想为核心,构建 **“空间+孵化+融资+政策+资源+AI赋能+综合服务”七位一体**全链条创业扶持体系,打造“一站式”创业孵化服务平台。", "8. 宣传册核心内容(园区简介·服务·政策)\n- 紧扣面向南亚东南亚的国际定位,融合AI前沿技术,打造现代化创业/创新服务平台,成为青年人才来昆留昆创业创新的“首站”与“家园”。", "### 8.2 政策服务(七大板块)", "8. 宣传册核心内容(园区简介·服务·政策)\n1. **空间保障**:免费办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等基础办公设施。", "8. 宣传册核心内容(园区简介·服务·政策)\n2. **孵化服务**:专业创业指导专家团队提供创业规划、技术指导、商业模式梳理、市场对接等“一对一”服务;定期举办创业沙龙、创业讲堂、项目路演。", "8. 宣传册核心内容(园区简介·服务·政策)\n3. **融资支持**:联动省内外创投机构,设立创业种子资金/合作投资基金,提供政策性贷款、商业银行贷款、股权融资等推荐指导。", "8. 宣传册核心内容(园区简介·服务·政策)\n4. **政策对接**:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策。", "8. 宣传册核心内容(园区简介·服务·政策)\n5. **资源整合**:对接驻昆高校、科研院所、龙头企业,提供技术合作、成果转化、人才招聘;纳入昆明创业创新服务体系,优先参与“春城创业荟”等品牌活动。", "8. 宣传册核心内容(园区简介·服务·政策)\n6. **专项赋能培训**:定期开展AI公益性培训,提供AI应用、数字内容创作、一人公司创业落地、企业AI降本增效等公益培训与实战辅导。", "8. 宣传册核心内容(园区简介·服务·政策)\n7. **其他服务**:提供工商注册、税务代办、知识产权咨询、法律援助等专业化服务。", "### 入驻条件(宣传册口径,与管理规程一致)\n(一)**对象范围**:在校大学生及毕业8年内高校毕业生、归国留学生、外籍留学生等青年创业企业(项目),申请人须担任法定代表人/项目负责人;优质项目或OPC企业可放宽。\n(二)**产业方向**:聚焦昆明市“10+N”产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等),具备创新性、可操作性、市场前景良好。\n(三)**其他要求**:明确可行的经营计划、", "启动资金和风险承担能力,规范管理制度;无知识产权纠纷、违法犯罪及经营异常记录。\n(四)**优先入驻**:获得市级及以上创业大赛奖项、突出学术成就、重大发明创造及高新专利技术的项目。", "### 入驻流程(宣传册六步)\n咨询报名 → 提出申请 → 项目初审 → 入驻评审(路演答辩,量化打分≥80分通过)→ 评审公示(市人社局官微公示)→ 签署协议、分配场地工位、正式入驻。\n---", "| 区域 | 对象 | 配套 | 周期 |\n|---|---|---|---|\n| **OPC创业空间** | AI为支撑,聚焦数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域 | 拎包入驻独立办公位或共享工位 | 最长2年 |\n| **创业苗圃区** | 创意阶段、未注册企业的大学生创业团队 | 开放式共享工位 | 最长2年(成长好可转加速区1年) |\n| **孵化加速区** | 已工商注册、", "初创期企业 | 独立办公空间 | 基础2年,评估合格延1年,累计≤3年 |\n| **国际创客区** | 归国留学生、外籍留学生 | 个性化办公空间+国际化配套 | 参照孵化加速区 |\n---", "11. 报名方式\n- 咨询电话:18087174891、18214573650(宣传册);18087174891(统计表填报人);云南省就业局/园区申报口径见第3节填报信息", "11. 报名方式\n- 咨询地址:昆明市民航路229号(昆明市人力资源中心二楼 昆明市大学生创业园)", "11. 报名方式\n- 接收邮箱:714066050@qq.com", "**媒体报道矩阵**:整合电视、广播、报纸、新媒体等资源,通过电视专栏、广播专题、报纸专版、网络直播、地铁专列、新媒体推广等方式,全方位宣传报道园区创业创新典型事迹。\n**合作媒体机构**(宣传册列示):", "12. 媒体报道与合作媒体\n- 全国主流:央视网(CCTV.com)、中国网(China.org.cn)、人民网(people.cn)、今日头条、网易、搜狐、抖音", "12. 媒体报道与合作媒体\n- 省级媒体:云南日报、云南广播电视台(Yunnan Media Group)、云南网、春城晚报", "12. 媒体报道与合作媒体\n- 市级媒体:昆明广播电视台、昆明日报、掌上春城(KMCC)、无线昆明", "12. 媒体报道与合作媒体\n- 其他:8099999 新闻热线等", "**重要外宣数据口径(宣传册/新闻引用,酌情采用)**:", "12. 媒体报道与合作媒体\n- 昆明市政府网站:园区运营超过10年,入驻企业超1000家、带动创业者超1万人次(2024-04报道口径)。", "12. 媒体报道与合作媒体\n- 春城晚报:近3年累计孵化企业超100家,培育国家级高新技术企业5家、省级科技型中小企业12家。", "12. 媒体报道与合作媒体\n- 昆明日报:2024年以来新增入驻企业32家、其中高新技术企业15家;举办各类活动126场,覆盖青年创业者超2万人次。", "12. 媒体报道与合作媒体\n- **提示**:以上为媒体报道口径,与省级申报/月报表中的官方累计口径(累计孵化181家、出园149家、带动就业1529人等)存在差异,对外引用时请注明来源。", "| 时间 | 事件 |\n|---|---|\n| 2009年9月 | 昆明市大学生创业园成立 |\n| 2012年 | 获评省级创业园 |\n| 2024-04-15 | 昆明市大学生创业园2026年第二期入驻招募公告发布(同时发布第一期评审结果公示) |\n| 2024-04-27前后 | 园区经全周期优化后“焕新亮相”获多家媒体报道(央视网5G、中国网、昆明市政府网、昆明日报、", "8099999等),报道时间多为2024-04-28 |\n| 2024-05-08 | “聚力赋能·共创未来”高校创新创业交流暨项目路演活动(主办:园区;协办:滇池学院、昆明城市学院、五华区大学生创业联盟) |\n| 2024-05-10 | 昆明城市学院师生赴园区“取经”参观交流 |\n| 2024-05-12 | 春城晚报、", "云南日报整版深度报道《创业有园 未来可期》 |\n| 2024-05-13 | “昆明市青年企业家协会走进园区暨创新创业基地揭牌仪式” |\n| 2024-05-14 | “云南高校·昆明市大学生创业园”就业创业交流活动暨创新创业基地揭牌仪式(云南经济管理学院、昆明技师学院80余人参加) |\n| 2026-03-13 | 《入驻管理规程》印发(昆明市人力资源和社会保障局) |\n| 2026-03-20 | 2026年第一期入驻申请受理截止:收到申请38个,25个通过初审、", "13个提交复审 |\n| 2026-03-24 | 第一期集中评审(30个路演参评,评审走“资格复核—现场路演—专家提问—综合评议”,量化打分) |\n| 2026-03-25 | 《工作简讯〔2026〕第1期》:第一期最终确定23个优质企业(项目)正式入驻 |\n| 2026-04-15 | 发布2026年第二期入驻招募公告 |\n| 2026-04-24 | “创赢未来”2026创业大赛昆明选拔赛暨马兰花创业培训讲师大赛落幕(主办:昆明市人社局;", "承办:昆明市公共就业和人才服务中心;4月20日起4天路演,13个项目晋级省级大赛) |\n| 2026-05-06 | 《工作简讯〔2026〕第2期》:此梳理第一期23家入驻企业全覆盖实地摸排,2家长期停滞企业督促整改 |\n| 2026-05-20 | “滇水逐梦·雨林创航”2026昆明·西双版纳创业路演PK赛(西双版纳州公共就业和人才服务中心、滇池学院、园区联合主办;园区企业鬼才明AI活动策划工作室、", "云南定影文化传播有限公司协办) |\n| 2026-06-03 | 云南旅游职业学院赴园区开展就业创业交流活动 |\n| 近期(2026) | 云南师范大学与园区联合举办就业创业交流活动暨就业创业实践基地揭牌仪式(开启“校地产教融合、跨境双创人才共育”,缅甸、越南、印尼、老挝等国留学生参加) |\n---", "> 出处:昆明市大学生创业园 定稿(.ai 源文件 + 制版PNG/JPG)", "14. 品牌VI设计规范\n- **Logo**:橙、蓝双色组合标识——橙色“K”与深蓝“M”交织成圆环形/动感造型,英文全称“KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK”,主推版式居中,品牌色橙蓝相衬。", "14. 品牌VI设计规范\n- **品牌主色(设计稿拾色器取值)**:", "| 色彩 | Hex | R | G | B | 说明 |\n|---|---|---|---|---|---|\n| 主色·橙 | `#FC8318` | 252 | 131 | 24 | 活力橙,主标识/强调 |\n| 辅色·深蓝 | `#022B97` | 2 | 43 | 151 | 深邃蓝,主标识/正文 |", "14. 品牌VI设计规范\n- **配套文件**:`昆明市大学生创业园 定稿.ai`(矢量源文件)、`_画板 1.png/.jpg`(主画板成品)、`_画板 1 副本.png`(色彩规范参考)。", "### 附注:数据口径说明", "14. 品牌VI设计规范\n1. **累计数与当年数**:第2节绩效、第3节月报表中均区分“认定以来累计 / 本年度 / 当前实有”。本月报表中累计出园149户与省级申报累计出园150户相差1户,为两表填报时点不同所致,均照原文保留。", "14. 品牌VI设计规范\n2. **入驻规模**:信息表(39家)面向内部全量项目;省级申报名单(31家)为认定口径,项目名称个别有别。", "14. 品牌VI设计规范\n3. **宣传册数据**(媒体报道/外宣口径)与官方申报累计口径存在差异,引用时务必注明出处(见第12节提示)。", "14. 品牌VI设计规范\n4. 本文件所列数值均直接源自原始文件,未作推算加工。", "---\n*文档生成日期:2026年 · 依据 `.xlsx / .xls / .wps / .pdf / .png` 全量原始资料整理*"], "questions": [["这个知识库里的政策和数据是最新版的吗?", "补贴金额和申报条件以哪里为准?", "你们说的截至2026年8月是什么意思?", "如果官方文件更新了,这里的内容还准不准?", "这个知识库主要是用来干啥的?"], ["一个人能开公司吗?", "什么叫一人公司?OPC是啥意思?", "一人有限责任公司和一人股份有限公司有啥区别?", "只有一个股东的公司合法吗?", "OPC是不是就是只有一个老板的公司?"], ["一人公司是啥时候开始在中国合法的?", "2005年《公司法》修订跟一人公司有啥关系?", "以前为啥不允许一个人开公司?", "一人有限责任公司最早是哪年写进法律的?", "中国是从什么时候起承认一人公司的?"], ["AI时代为啥适合搞一人公司?", "一个人用AI能干成哪些事?", "现在一个人创业靠AI真能搞定研发、营销、客服和财务吗?", "为啥说一人公司是低成本高效率的创业方式?", "AI时代的一人公司跟传统小作坊有啥不一样?"], ["‘你负责商业判断,AI负责高效执行’这句话啥意思?", "一人公司怎么从卖时间变成经营系统?", "怎么理解‘经营自动化系统’这个说法?", "一人公司里人和AI具体怎么分工?", "这句话说的‘跨越’到底指什么变化?"], ["一人公司和自由职业者有啥区别?", "OPC算不算正规公司?跟个体户、 freelancer比有啥优势?", "一人公司是不是有限责任?架构是不是特别简单?", "为啥说一人公司税负更可控?它和普通公司比灵活在哪?"], ["现在全国有多少家一人公司?", "截至2025年6月,一人有限责任公司总数大概多少?", "一人公司数量破千万了吗?最新统计数据是多少?", "全国一人公司注册量现在到什么规模了?"], ["新《公司法》对一人公司是放宽还是收紧了?", "2024年7月1日实施的新公司法,对一人公司主要有哪些变化?", "新公司法下一人公司是更容易注册了还是责任更大了?", "新《公司法》对一人公司的总原则是什么?"], ["一个人能开股份有限公司吗?", "新公司法允许一个自然人设立股份有限公司吗?", "一人股份公司合法吗?需要几个股东才能成立股份公司?", "以前说股份公司至少两人,现在改了吗?"], ["一个人还能注册多家一人公司吗?", "新公司法取消了‘一个自然人只能开一家一人公司’的规定吗?", "我之前注册了一人公司,还能再注册第二家吗?", "一人公司之间还能互相投资成立新公司吗?"], ["新公司法实施后,个体工商户能自己开一人公司吗?", "合伙企业可以当一人公司的股东吗?", "个人独资企业现在能注册成一人公司的老板不?", "2024年7月1号起,除了自然人和法人,还有哪些主体能当一人公司股东?", "新《公司法》是不是把一人公司股东范围扩大到个体户、合伙企业这些了?"], ["新公司法还分‘一人有限责任公司’这种专门类型吗?", "现在注册只有一个股东的公司,营业执照上还写‘一人有限公司’吗?", "新法里是不是取消了‘一人股份有限公司’的说法?", "只有一个股东的公司,现在法律上怎么称呼?还叫‘一人公司’吗?", "新《公司法》是不是把一人公司直接当成普通公司来管,不再单列一节了?"], ["新公司法下,一人公司每年还要强制做审计吗?", "只有一个股东的公司,2024年7月后还必须请会计师事务所审账吗?", "现在一人公司还能自己编财务报告,不用找人审计不?", "新《公司法》取消了对一人公司的年度审计硬性要求,是真的吗?", "一人公司今年开始是不是不用每年都搞财务审计了?"], ["新公司法实施后,营业执照上还会标‘自然人独资’或‘法人独资’吗?", "注册一人公司时,登记信息里还要特别注明是独资性质吗?", "现在办营业执照,系统还让选‘自然人独资’这个选项吗?", "一人公司的工商登记和执照上,是不是不再体现‘独资’字样了?", "新法取消了独资标注要求,那别人怎么知道这家公司是一个人开的?"], ["新《公司法》对一人公司最大的责任变化是什么?", "一人公司股东现在要承担什么更强的责任?", "听说新公司法加强了一人公司的股东责任,具体指哪块?", "为什么说‘责任强化’是重点注意事项?会带来什么实际影响?", "一人公司老板在新法下要特别注意哪些法律责任风险?"], ["新公司法实施后,一人公司的股东如果没法证明公司财产和自己个人财产分开,是不是要对公司债务一起负责?", "2024年7月1日新公司法生效后,一人公司股东举证责任倒置具体指什么?", "一人有限公司的老板要是说不清公司钱和自己钱的区别,会不会被要求连带还债?", "新《公司法》第23条第三款关于一人公司股东连带责任的规定,到底怎么理解?", "一人公司股东怎么避免因财产不独立而承担连带责任?新法里举证责任倒置是啥意思?"], ["法院一般怎么判断一人公司存在财产混同?账户之间转钱多算不算?", "公司账上和个人银行卡老有资金往来,会被认定为财产混同吗?", "一人公司股东和公司之间经常互相转账,是不是很容易被认定为财产混同?", "司法实践中,认定一人公司财产混同最主要看什么?是不是就看银行流水?", "公司跟股东之间频繁打款,是不是财产混同最直接的证据?"], ["用我自己的微信/支付宝订阅ChatGPT Plus,算不算公司财产不独立?", "AI时代下,用个人账号买大模型服务、存训练数据,会不会让一人公司被认定为没独立财产?", "我把公司用的AI算力、生成内容都存在个人云盘里,新公司法下有风险吗?", "以个人名义开通的云端GPU服务,给公司项目用,会被认为财产混同吗?", "在个人账号里管理公司AI资产(比如API密钥、训练数据、生成结果),新法实施后会否导致股东担责?"], ["一人公司没有股东会,利润怎么分?必须写成书面文件吗?", "新公司法要求一人公司股东做决定必须签字留底,哪些事必须书面决定?", "一人公司修改章程或分利润,不开会的话,光口头说行不行?", "股东自己决定分红或补亏,是不是得签个字或者盖个章,然后放在公司备查?", "新《公司法》规定一人公司重大事项须书面决定,具体包括哪些?怎么操作才合规?"], ["一人公司要怎么管账才算符合新公司法要求?", "新公司法强调财务独立,一人公司是不是必须单独建账、单独报税?", "公司账和老板私人账混着记,新法下有什么风险?", "一人公司如何做到账目清晰、独立核算?有没有实操建议?", "建立健全财务管理制度对一人公司来说,具体要落实到哪些动作上?"], ["新公司法实施后,一人公司怎么证明财产独立?需要做哪些审计?", "2024年7月1日新公司法生效后,一人有限公司必须每年做审计报告吗?", "一人公司股东和公司之间资金往来,要怎么审计才能避免连带责任?", "新《公司法》要求一人公司提供连续完整的会计年度审计报告,具体指什么?", "除了年度审计,一人公司还能用什么审计报告来证明财产独立?"], ["新公司法说的‘数据与数字资产隔离’是什么意思?跟以前的财务隔离有啥不同?", "一人公司现在不仅要管好钱,还要管好数字资产?具体要怎么做?", "新《公司法》对一人公司的合规要求是不是从钱扩展到数据了?", "什么叫‘数字资产隔离’?一人公司怎么确保客户数据、AI模型这些不和股东个人混同?", "新公司法提到的数据隔离,是指公司微信账号、域名、数据库这些也要和股东分开吗?"], ["AI时代创业还用等学完所有技能再开始吗?", "听说现在创业讲究‘先完成再完美’,是不是真的不用准备充分就能启动?", "AI把创业门槛降低了,那新手到底该什么时候开始做第一个项目?", "‘先完成,再完美’在OPC创业里具体怎么操作?举个例子?", "不会编程、不会设计,能直接用AI开公司吗?需要先学多久?"], ["最小成本验证模式具体怎么操作?比如第一步该做什么?", "用AI做决策+外包干活,这种最小验证方式适合哪些类型的创业项目?", "没多少启动资金,怎么用最小成本验证我的商业想法靠不靠谱?", "OPC创业中说的‘最小成本验证’,和MVP(最小可行产品)是一回事吗?", "AI思考+外包人力,这个组合在验证阶段容易踩哪些坑?"], ["OPC创业说的‘四力’是哪四力?分别指什么?", "产品能力、流量能力、交付能力、变现能力,这四个能力怎么判断自己有没有?", "创业初期资源有限,应该优先提升四力中的哪一力?", "为什么OPC特别强调这四个能力?少了其中一力会有什么风险?", "怎么评估自己的流量能力和变现能力是否匹配?有没有简单自测方法?"], ["盈利三把钥匙具体指哪三个?", "商业模式里说的‘盈利三把钥匙’是什么意思?", "怎么理解用AI降本增效、用信任创造溢价、用平台借势腾飞这三句话?", "OPC提到的盈利三把钥匙,有没有更接地气的解释?", "盈利三把钥匙和普通创业方法有啥不一样?"], ["OPC推荐的主流商业模式有哪些?", "做个人创业,有哪些靠谱的变现方式?", "知识付费、内容变现、服务外包这些模式具体都包括啥?", "SaaS工具、付费社群、混合模式分别适合什么人做?", "有没有把六大商业模式讲清楚的例子或对比?"], ["OPC创业落地的完整步骤是怎样的?", "从零开始做个人创业,该按什么顺序一步步来?", "怎么用最低成本验证一个想法靠不靠谱?", "什么是MVP测试?私域池和自动化引流矩阵怎么搭?", "怎么设计能抗风险的收入结构?标准化交付系统怎么做?"], ["电商创业案例里那个河南创业者是怎么做到月入几万的?", "卧床70天还能做跨境电商?他具体用了哪些AI工具?", "用扣子搭工作流生成经营日报,这个怎么操作?", "12家网店为啥最后只留4家?优化逻辑是啥?", "每天只干2-3小时,单店月净利2.5万以上,普通人能复制吗?"], ["盈利三把钥匙和六大商业模式之间有啥关系?", "创业步骤里提到的‘高溢价产品矩阵’怎么跟‘用信任创造溢价’挂钩?", "AI降本增效在河南电商案例里是怎么体现的?", "自动化引流矩阵和内容变现、社群运营这些模式怎么配合?", "整个OPC创业思路里,最核心、最先该动手的是哪一步?"], ["OPC创业做小红书内容转化服务,具体怎么帮企业提升转化?2025年无锡创业者转型‘企业陪跑’,定价和交易机制这两块到底该怎么解决?", "无锡那边有创业者在2025年搞OPC小红书陪跑服务,听说卡在定价和交付标准化上——这俩难题实际中怎么破?", "OPC模式里说‘个人即产品’、前期没标准交付物,那小红书内容转化服务怎么跟客户谈价格、签合同?", "做企业小红书陪跑的OPC项目,除了内容制作,还包含哪些服务?定价逻辑和交易流程一般怎么设计?", "听说OPC创业在小红书转化这块,最大痛点是定价难+没标准化交付——有没有落地的解决方案或参考案例?"], ["那个全AI员工的公司是怎么运作的?用什么开源AI代理框架搭的?卖教程、抽佣、定制代理这三块业务具体怎么赚钱?", "零人类公司几周赚近20万美元,每月成本才1500美元?它靠哪些AI工具实现全自动运营?教程和技能市场怎么搭建的?", "用开源AI代理做的‘全AI员工公司’,真实案例里AI到底负责哪些环节?企业定制代理和抽佣平台怎么落地?", "不雇人、纯AI跑的公司,收入来源是不是就教程销售、AI技能市场佣金、企业代理定制这三项?每块大概占比多少?", "全AI运营公司月成本只要1500美元,它用的是LangChain还是AutoGen这类框架?技术栈和商业模型能拆解下吗?"], ["35岁大厂管理者裸辞环游世界后做一人公司,具体靠哪些AI相关业务年入近百万?都干了啥角色?", "裸辞后靠AI搞一人公司,同时当大模型工具开发者、企业顾问和自媒体博主——这三重身份怎么协同变现?", "大厂裸辞转AI一人公司,年入百万的真实路径是啥?开发工具、接咨询、做自媒体,哪块收入占比最高?", "35岁裸辞环游多国还能靠AI年入百万,他做的大模型工具有哪些?企业咨询主要解决什么问题?", "一人公司模式下,怎么平衡大模型开发、企业顾问和自媒体三件事?时间分配和收入结构大概是怎样的?"], ["OPC创业里提到的‘清醒认知与挑战’具体指哪些方面?新手最容易踩哪些坑?", "关于OPC创业的清醒认知部分,有没有总结过核心挑战点?比如心态、资源、能力短板这些?", "‘清醒认知与挑战’这块没展开说,实际做OPC时最常被忽略的认知盲区是什么?", "OPC创业者需要具备哪些关键清醒认知?比如对自身能力、市场周期、失败概率的预判?", "除了赚钱门槛没消失,OPC路上还有哪些隐性挑战?比如客户信任建立、长期可持续性、精力管理?"], ["为什么说AI没消灭赚钱门槛?光有工具但没商业逻辑,到底会出什么问题?能举个实际例子吗?", "AI再强也得靠认知和决策能力——这话怎么理解?有没有因为商业逻辑错导致AI项目失败的案例?", "没有商业思维,只堆AI工具会怎样?比如选错目标客户、定价失衡、交付失控这些具体表现?", "AI创业里,‘运气’到底起多大作用?除了认知和决策,还有哪些不可控但关键的因素?", "为什么说决策能力和运气和AI工具一样重要?在OPC实战中,哪些关键决策点最容易翻车?"], ["网上说一人公司月入十万很轻松,真的靠谱吗?", "那些'一人公司轻松月入十万'的说法是不是夸大其词?", "做OPC创业真能随便月入十万?靠谱吗?", "听说有人靠一人公司月入十万,这现实吗?", "OPC创业收入是不是被网上吹得太神了?"], ["一个人开OPC公司,所有事情都要自己干吗?比如收款、定价这些?", "OPC创业是不是啥都得自己扛,连回款和现金流都要亲力亲为?", "当超级个体户,财务、定价、催款这些活儿都得自己来?", "一人公司是不是从接单到收钱、定价格全得自己操心?", "做OPC会不会特别孤独,而且所有运营事务都得自己搞定?"], ["云南省有没有专门支持AI方向OPC创业的新政策?是哪年发布的?", "2026年云南人社厅出了个AI OPC创业支持文件,具体叫什么名字?", "云南省关于人工智能OPC创业的省级专项政策是哪个部门发的?", "听说云南今年出了个AI OPC扶持政策,是人社厅出的吗?几月份的?", "云南省最新的人工智能OPC创业支持政策,全称和发布时间是什么?"], ["云南现在有没有专门为AI OPC创业者建的生态社区?里面提供啥?", "云南的AI OPC创业生态社区能提供算力、绿电这些资源吗?", "云南打造的AI OPC创业社区是不是包含免费空间、普惠算力和全链条服务?", "云南的AI OPC生态社区怎么申请?有补贴吗?", "云南鼓励用高校场地建AI OPC社区,里面整合了哪些要素?能拿补助吗?"], ["云南OPC创业者能申请免费工位吗?在哪儿申请?", "OPC创业者在云南园区能拎包入住免费工位?需要什么条件?", "云南有没有给OPC人才提供的免租公寓或青年驿站?", "在云南做超级个体户,能住人才公寓或者青年驿站吗?收费吗?", "云南园区OPC专区的免费工位和安居支持(比如低租公寓)具体怎么落实?"], ["云南现在对超级个体户有啥贷款政策?云岭创业贷能贷多少?算力贷和数智创新专项信贷是啥?", "我是个人创业者,想在云南申请信用贷款或抵押贷款,最高能贷多少?有没有专门支持AI创业的贷款产品?", "云南OPC超级个体政策里的金融支持包括哪些?企业法人和个人创业者分别能贷多少?", "云岭创业贷对企业法人和个人创业者额度一样吗?3000万抵押贷是针对企业的还是个人的?", "听说云南有‘算力贷’和‘数智创新专项信贷’,这些贷款怎么申请?需要啥条件?"], ["云南给创业者的补贴有多少钱?一次性创业补贴是5000块吗?电商创业能拿多少?", "开网店算电商创业吗?在云南做电商能领最高3万元补贴?要满足啥条件?", "云南OPC政策里的一次性创业补贴和电商项目补贴能同时拿吗?", "刚注册个体户能申5000块创业补贴吗?电商补贴是按项目还是按人发?", "云南对超级个体户的创业补贴具体咋发?是直接打钱还是报销?电商补贴要提供哪些材料?"], ["云南企业招高校毕业生见习,每月补贴多少钱?留用率超一半真能涨到2000块?", "我们公司接收应届生见习,每人每月能领多少见习补贴?怎么才算留用率达50%以上?", "云南OPC政策里的见习补贴是给企业还是给学生?1500元和2000元的区别到底在哪?", "高校毕业生见习期一般多长?补贴是按月发还是一次性结清?", "吸纳见习人员后,企业怎么申请这个见习补贴?需要签合同吗?"], ["学AI相关技能比如数据标注,在云南培训能拿补贴吗?最多补多少?", "数据标注员培训补贴是7280元?创业培训又是1700元?这两个能一起申吗?", "云南对AI职业的培训补贴覆盖哪些工种?除了数据标注还有哪些算AI相关?", "参加政府认可的AI培训班,补贴是直接发给个人还是给培训机构?", "创业培训补贴和技能培训补贴申请条件一样吗?需要先就业还是可以零基础报名?"], ["云南AI领域的超级个体创始人能评职称吗?人工智能专业职称怎么评?", "没学历、年纪大、没资历,能在云南评AI方向的人才称号吗?兴滇英才计划怎么申报?", "OPC创始人进‘兴滇英才支持计划’有啥好处?人才自主评价机制具体怎么操作?", "人工智能专业职称评审打破学历限制是真的吗?需要发表论文或专利吗?", "云南对AI创业者的省级人才认定有哪些?除了兴滇英才还有别的计划吗?"], ["云南现在办OPC超级个体营业执照,能一次搞定所有手续吗?", "在云南注册OPC超级个体,是不是填一张表、上一个网站就能办完?", "云南的‘个人创业一件事’集成服务具体怎么操作?", "OPC超级个体在云南能不能一网通办、一次办结?", "云南对OPC超级个体有没有政务服务优化措施?"], ["昆明市大学生创业园属于市级还是省级的孵化平台?", "昆明市大学生创业园是哪个层级的创业支持载体?", "想申请昆明市大学生创业园入驻,这个园区归谁管?", "昆明市大学生创业园是不是市级政府主办的?", "云南的OPC政策里提到的市级创业园具体指哪个?"], ["云南OPC超级个体政策面向哪些年轻人?", "在校大学生和毕业几年内的毕业生能申请云南OPC吗?", "留学生包括归国和外籍的,能不能享受云南OPC政策?", "毕业超过5年但没到8年的高校毕业生,还符合云南OPC申请条件吗?", "云南OPC政策里说的‘青年创业者’具体包括哪些人群?"], ["云南OPC超级个体项目必须做哪些行业才算符合要求?", "昆明市‘10+N’产业链都包括哪些方向?比如人工智能算不算?", "做新媒体直播或生物医药的项目,能不能申请云南OPC?", "拿过市级创业比赛奖的项目,在云南OPC入驻时有优先权吗?", "有发明专利或者学术成果,申请云南OPC会不会更容易通过?"], ["云南OPC超级个体能免费用办公场地吗?", "入驻云南OPC有没有工位或办公室提供?", "OPC超级个体在云南能申请免费独立办公区吗?", "云南给OPC创业者提供的是共享工位还是单间?", "云南OPC政策里的空间保障具体是免费提供什么?"], ["云南OPC超级个体政策里,孵化服务具体包括哪些内容?", "创业初期能获得哪些一对一指导?", "有没有商业模式梳理和路演机会?", "云南的OPC孵化服务包含专家辅导吗?", "想参加创业沙龙或项目路演,云南OPC政策支持吗?"], ["云南OPC政策有融资支持吗?具体怎么帮创业者找钱?", "种子资金是政府出的还是创投机构出的?", "每年组织多少场融资对接活动?", "OPC创业者能参加融资路演或对接会吗?", "云南有没有专门针对超级个体的早期投资渠道?"], ["云南OPC政策能帮忙申请哪些创业扶持资金?", "税收减免和社保补贴具体怎么申请?", "政策对接服务包括哪些实际帮扶?", "个人创业者在云南能享受哪些财政补贴?", "OPC个体户可以申请减税或社保补助吗?"], ["云南OPC政策提到资源整合,都对接哪些单位?", "高校、科研院所和大企业怎么跟OPC创业者对接?", "作为个体创业者,能通过OPC链接到龙头企业资源吗?", "云南OPC能帮我联系大学或研究院合作吗?", "政策里的‘资源整合’是指拉通产学研资源吗?"], ["云南OPC的AI育笋计划是免费的吗?都教什么?", "‘AI+OPC’培训包含一人公司注册和成本控制吗?", "有没有面向超级个体的AI应用实操课?", "AI育笋计划和AI+OPC培训是一回事吗?", "免费AI培训覆盖从入门到落地的全流程吗?"], ["云南现在OPC超级个体政策有哪些服务?", "在云南做OPC超级个体,能享受哪些代办和咨询服务?", "云南OPC政策里说的一站式支持都包括啥?工商税务知识产权法律这些都能帮办吗?", "云南OPC超级个体能免费办营业执照和报税吗?", "想在云南搞OPC,工商注册、税务、法务、知产这些事有人全程代办吗?"], ["OPC的核心范式是什么?", "OPC最根本的运作方式或底层逻辑是啥?", "什么叫OPC核心范式?能用大白话解释一下吗?", "OPC和其他创业模式最大的区别在哪?是不是就靠那个核心范式?", "OPC强调的‘核心范式’具体指哪一套方法论或结构?"], ["OPC创业是不是只要1个人加几个AI就能干?创始人和AI各自干啥?", "OPC的1+N模式里,人负责什么、AI又干哪些活?", "听说OPC是‘1个创始人+N个AI员工’,这N个AI到底做哪些重复性工作?", "OPC中创始人是不是只管拍板和创意,其他都甩给AI?", "OPC的1+N模式具体怎么分工?比如写代码、做客服、搞运营这些谁来干?"], ["OPC创业为啥说从资本密集变成认知密集了?", "不融资、不招人,光靠脑子就能创业?OPC真这么轻松?", "OPC强调认知密集型创业,是不是意味着懂行比有钱更重要?", "OPC不需要团队和投资,那靠啥赚钱?是不是全看创始人对行业的理解深度?", "为什么说OPC把创业重心从砸钱变成了拼认知?有没有实际例子?"], ["OPC创业门槛真的变低了吗?现在开公司还需要懂编程会外语吗?", "OPC说门槛从资源降到认知,具体哪些事AI能替我搞定?", "不会写代码、英语不好、没客户资源,还能做OPC吗?AI能帮我补上这些短板吗?", "用Claude写代码、AI生成内容、实时翻译、AI筛选客户——这些在OPC里都是标配吗?", "OPC里提到的AI工具链(比如Claude Code、AI Agent)普通人能直接上手用吗?需要技术基础吗?"], ["OPC目前有哪些已经落地验证的商业模式?", "OPC跑通了哪六种商业路径?", "哪些OPC商业模式已经被实际验证可行?", "园区说的六大已跑通的OPC模式具体指哪些?", "OPC领域里哪些商业模式已经有成功案例了?"], ["专业服务型OPC具体是做什么的?", "靠专业技能接项目、按小时或项目收费的OPC模式叫什么?", "深度定制化咨询、高利润率的OPC创业方式是什么?", "以专业能力为核心、做定制方案的OPC类型怎么称呼?", "OPC里那种靠专家经验卖服务、不卖产品的模式叫啥?"], ["高端服务型OPC适合什么人做?", "一单能收几十万的战略顾问、私教类OPC属于哪种模式?", "靠行业洞察和战略判断力赚钱的OPC类型叫什么?", "OPC中主打高端顾问、私董会、战略规划的是哪一类?", "强调认知高度和决策能力、客单价特别高的OPC模式是哪种?"], ["知识产品型OPC主要卖什么?", "把专业知识做成课程、电子书、会员的OPC模式叫什么?", "边际成本几乎为零、靠标准化内容变现的OPC类型是哪种?", "系统化知识打包成数字产品的OPC创业方式怎么称呼?", "为什么说知识产品型OPC最被看好?它具体怎么做?"], ["数字产品型OPC包括哪些具体形式?", "SaaS、模板、插件这类可复用工具属于哪种OPC模式?", "靠订阅制或API收费的软件类OPC创业方式叫什么?", "能规模化复制、有最大增长潜力的OPC类型是哪一种?", "OPC里做应用、工具、自动化模板的是哪种模式?"], ["OPC创业里,靠写博客、拍视频、做社交媒体赚钱属于哪种模式?", "如果我主要靠个人IP接广告、做知识付费,这算OPC里的什么创业类型?", "创意内容型OPC具体怎么变现?有哪些常见方式?", "做自媒体、短视频、小红书博主,属于OPC五种模式里的哪一种?", "靠影响力产出内容并赚钱,是OPC中的‘创意内容型’吗?"], ["OPC里不自己生产产品、只帮别人牵线搭桥收佣金的模式叫什么?", "做中间人撮合资源、收服务费或佣金,属于OPC的哪种创业方式?", "平台整合型OPC是不是就是当‘二道贩子’或者‘中介’?", "没有自有产品,靠连接供需双方赚钱,这是OPC里的平台整合型吗?", "OPC中强调资源对接和撮合收费的模式具体指什么?"], ["怎么判断OPC哪种创业模式最适合我?", "新手想入局OPC,该怎么选适合自己的创业路径?", "有没有简单的方法帮我决定该走内容型、顾问型还是平台型?", "选择OPC创业模式时需要考虑哪些关键因素?", "如何结合自身情况匹配最合适的OPC模式?"], ["OPC创业前必须搞清楚的三个核心问题是什么?", "卖能力还是卖系统?服务边界清不清楚?执行者还是系统设计师?这三个问题到底在问什么?", "OPC选模式时,为什么要想清楚自己是执行者还是系统设计师?", "‘服务边界清不清楚’在OPC创业中具体指什么?怎么判断?", "OPC里说的‘卖能力’和‘卖系统’有什么区别?举个例子?"], ["OPC中顾问或咨询型适合什么样的人?", "专业强、人脉广的人更适合OPC里的哪种创业模式?", "AI能帮OPC顾问型创业者做哪些事?比如找客户、写方案?", "做咨询类OPC,AI现在能替代哪些重复性工作?", "如果我有多年行业经验但不想天天干活,OPC顾问型+AI辅助靠谱吗?"], ["我想靠写文章、做短视频创业,有什么适合个人IP的轻量级创业方式?", "有没有那种内容能反复用、发多个平台还不怎么花钱的创业方法?", "我擅长总结知识和表达,有什么创业模式特别适合我这种人?", "做知识类自媒体,怎么把内容变成能长期赚钱的生意?", "个人IP创业是不是就是做公众号、小红书、抖音这些?还有没有别的玩法?"], ["怎么把我的咨询经验变成一个能自动卖的产品?", "有没有办法把服务做成像软件那样一次开发、多次销售?", "想从接单做项目转成卖标准化数字产品,该怎么做?", "SaaS或者数字工具这类产品化服务创业,难在哪里?", "把专业方法论打包成模板或工具卖,具体要怎么操作?"], ["现在哪些行业特别适合一个人创业做OPC?", "健康、教育、企业服务这些热门赛道,普通人入局机会大吗?", "做心理支持或IoT硬件方向的一人公司,需要什么基础?", "金融、法律这类专业服务,一个人能干得起来吗?", "机器人、SaaS、知识付费……哪些赛道更适合新手试水OPC?"], ["刚想搞一人公司,是应该先辞职全职干,还是边上班边试试?", "副业验证模式具体指什么?要做多久才算验证成功?", "不辞职直接注册公司行不行?风险大不大?", "为什么说别一上来就辞职做OPC?有没有真实踩坑案例?", "副业阶段该重点测试哪些东西,才能判断值不值得转正职?"], ["个体户和有限公司开一人公司,到底选哪个好?", "刚起步接小活,注册个体户是不是最省事省钱?", "想以后跟企业合作,是不是必须注册有限责任公司?", "一人有限公司怎么避免老板个人财产和公司财产混在一起?", "个体户税少但信用弱,有限公司信用好但麻烦多,怎么平衡?"], ["成立一人公司,是注册有限公司好还是个体户好?", "B端长期合作该选有限公司还是个体户?", "刚创业做产品验证,适合注册个体户还是有限公司?", "一人公司选有限公司和个体户各有什么优缺点?", "面向企业客户长期合作,注册一人有限公司合适吗?"], ["公司起名要准备几个备选名字?", "注册一人公司时,公司名称怎么取才容易通过?", "在‘一网通办’上核名要填哪些信息?", "公司名称格式是不是必须包含行政区划、字号、行业和‘有限公司’?", "市场监管局官网核名需要提交多少个名称备选?"], ["注册一人公司线上要提交哪些材料?", "设立登记申请要准备公司章程、身份证和地址证明吗?", "网上提交一人公司注册申请后多久能拿到营业执照?", "注册一人有限公司,线上办理大概要几天?", "营业执照一般多久能下来?"], ["一人公司刻章要刻哪几枚章?", "公章、财务章、法人章都要刻吗?去哪刻?", "刻公司印章要多少钱?大概费用是多少?", "公安备案的刻章点在哪里找?需要预约吗?", "刻章必须去公安指定地点吗?费用大概500块对吗?"], ["一人公司开基本户要带什么材料?", "银行开户要预约吗?需要法人本人去吗?", "开公司对公账户年费一般是多少?", "营业执照、公章和法人身份证都带齐了就能当场开户吗?", "基本户年费500到1000元,是所有银行都这样吗?"], ["领取营业执照后多久要办税务登记?", "公司注册完要多久去税务局报到?", "办完执照后30天内必须做税务登记吗?", "税务登记要核定哪些税种?", "新公司怎么申请发票?"], ["一个人能当公司法人吗?", "一人公司还需要设监事吗?", "现在一个人开公司,自己当老板还要请别人当监事吗?", "新公司法允许只有一名股东兼法人吗?", "一人有限公司的法定代表人可以是唯一股东吗?"], ["注册公司对注册资本有啥要求?", "注册地址有什么规定?", "公司注册时地址必须是商用吗?", "注册资本和注册地址分别要注意啥?", "注册公司时地址和资金方面有哪些硬性条件?"], ["一人公司注册时要马上把钱打到公司账户吗?", "认缴制下注册资金是不是可以先不交?", "公司章程里要写清楚多久交齐注册资本吗?", "认缴期限最长能写几年?", "5年内必须把认缴的注册资本全部实缴到位吗?"], ["认缴的注册资本没交完会有什么风险?", "注册资本写太高会有什么后果?", "公司亏了,股东要拿自己钱补注册资本吗?", "‘认缴’是不是等于不用缴?", "如果公司经营不善,股东要按认缴金额赔钱吗?"], ["注册一人公司时,住宅地址能当注册地址吗?", "一人有限责任公司可以用家里地址注册吗?", "注册OPC必须用商用地址吗?住宅行不行?", "写字楼、商铺、共享办公这些算商用地址吗?", "虚拟地址和集中登记地属于商用地址吗?"], ["园区提供的共享工位能用来注册公司吗?", "租用共享办公空间可以作为公司注册地址吗?", "集中登记地或虚拟地址要收费吗?大概多少钱?", "有没有免费的虚拟地址?园区会提供吗?", "3000到6000元一年的虚拟地址服务靠谱吗?"], ["公司没业务也要做账报税吗?", "零收入的一人公司需要记账和报税吗?", "不经营的OPC还要请会计或者报税吗?", "记账报税具体包括哪些事情?", "合规要求里提到的‘合规’主要指哪些方面?"], ["一人公司不赚钱也要做账吗?", "零申报是什么意思?怎么操作?", "不做账或漏报税会有什么后果?", "按月报还是按季报?怎么选?", "被列入经营异常名录会影响个人信用吗?"], ["小规模纳税人开普票,季度30万以内真的不用交增值税吗?", "小微企业企业所得税5%的优惠能持续到什么时候?", "年利润300万以下的企业所得税怎么算?", "2027年底前的税收优惠包括哪些?", "开专票也享受30万免征增值税吗?"], ["一人公司分红要交多少税?", "OPC公司分给老板的钱要扣多少个税?", "一人有限责任公司利润分配后要缴20%个税吗?", "股东从一人公司拿分红,个税怎么算?", "一人公司税后利润转到个人账户要交20%税吗?"], ["代账公司一个月收多少钱?", "刚成立的一人公司请代账大概要花多少?", "初创期找代理记账贵不贵?一般多少钱一个月?", "OPC公司起步阶段建议自己做账还是找代账?费用多少?", "代理记账200到500元/月,是按年付还是一月一结?"], ["一人公司年报什么时候交?", "OPC公司每年必须报年报吗?截止日期是几号?", "国家企业信用信息公示系统年报在哪填?几月几号前完成?", "6月30日前没提交年报会怎样?", "一人有限责任公司年报提交入口和时间要求是什么?"], ["一人公司能不能用老板的私人银行卡收公司款?", "公私账户混用会有啥风险?", "为什么说公司钱和老板钱要分开?不分开会担责吗?", "股东个人账户和公司账户混在一起,会被追债吗?", "如何避免一人公司股东对公司债务承担连带责任?"], ["选AI工具是看名气大不大还是看合不合适?", "不同任务该挑哪个AI工具?有没有通用最强的模型?", "为什么不能光挑最新最火的AI用?", "AI工具怎么选才不踩坑?是不是得先想清楚用在啥场景?", "有没有一款AI能搞定所有工作?选工具到底该看啥?"], ["AI在园区里主要用来干啥?", "园区里大家最常用AI来做什么?", "AI在咱们这儿是写代码多还是做内容多?", "为啥说AI在园区首先是编程助手?", "AI在软件开发和内容创作之间,园区更侧重哪个?"], ["选AI工具时最看重什么?", "什么样的AI平台更适合做自动化流程?", "怎么判断一个AI平台能不能接API或者装插件?", "工作流编排是什么意思?哪些AI支持这个功能?", "生态开放的AI工具有哪些特点?"], ["写代码用哪个AI工具好?", "DeepSeek和智谱GLM哪个更适合程序员?", "有没有开源又便宜的编程AI推荐?", "能自己部署的编程AI有哪些?", "DeepSeek和GLM在性价比和私有化方面各有什么优势?"], ["中文场景下用什么AI最合适?", "通义千问适合哪些具体用途?", "阿里云百炼和通义千问是什么关系?", "长上下文对日常使用有啥帮助?", "通义千问在云服务集成方面强不强?"], ["豆包能免费用吗?适合做什么?", "字节的豆包能生成视频和图片吗?", "豆包和通义、DeepSeek比起来有啥不同?", "创意类工作比如做海报、剪视频,用哪个AI好?", "C端免费的多模态AI工具推荐哪个?"], ["Kimi适合处理哪些类型的文档?", "长篇报告或研报用什么AI工具处理比较好?", "有没有能处理超长文本的AI工具推荐?", "处理几十页PDF文档该用哪个AI?", "Kimi和其它大模型比,长文本处理有什么优势?"], ["政企单位用什么AI工具比较合规?", "央国企内部推荐用哪个国产AI?", "需要资质认证的场景该选文心一言还是讯飞星火?", "百度和科大讯飞的AI工具在政务场景下怎么选?", "做政府项目时,语音识别和信创适配哪个AI更合适?"], ["微信里能直接用的AI工具有哪些?", "混元在微信生态里怎么用?", "腾讯混元适合做快速决策还是深度分析?", "有没有支持微信公众号/小程序集成的AI工具?", "混元的‘快慢思考’具体指什么功能?"], ["有没有好上手的AI工作流搭建平台?", "非技术人员怎么自己搭AI流程?", "什么是Agent平台?园区推荐哪个?", "AI自动化流程该用什么工具来设计?", "工作流和智能体(Agent)平台有什么区别?"], ["Coze扣子是哪家公司的产品?", "零代码搭建AI机器人用什么平台?", "扣子支持接入哪些大模型?比如豆包、Kimi这些能用吗?", "个人用户想做个自动问答机器人,用扣子方便吗?", "拖拽式AI Agent平台除了扣子还有别的推荐吗?"], ["Dify适合什么类型的企业用?", "Dify能支持私有化部署吗?", "Dify支持哪些大模型?", "Dify是偏技术还是偏业务的AI工具?", "Dify的工作流节点多不多?"], ["用无代码平台搭智能体要花多少钱?", "做个客服智能体一个月大概多少钱?", "无代码做内容/竞品/线索智能体贵不贵?", "有没有便宜的AI智能体搭建方式?", "170元能搞定哪些AI智能体?"], ["AI智能体和普通聊天机器人有啥区别?", "AI智能体是不是就是高级点的Chatbot?", "什么叫‘闭环系统’的AI智能体?", "AI智能体能自己定目标和改方案吗?", "为什么说AI智能体比对话机器人更智能?"], ["AI智能体由哪几部分组成?", "大模型+记忆+规划+工具调用,这四个缺一不可吗?", "没有记忆或不会调用工具还算AI智能体吗?", "AI智能体的核心公式是什么?", "‘主动规划’在AI智能体里指什么?"], ["AI智能体的三个关键能力是啥?", "智能体怎么实现自主决策?", "AI智能体能自动发邮件或查网页吗?", "它能一边干活一边优化自己的方法吗?", "工具调用具体包括哪些操作?"], ["智能体一般由哪几个核心部分组成?", "一个成熟的AI智能体包含哪些关键模块?", "AI智能体的五大组成部分是什么?", "智能体的‘大脑’指的是什么?它还有哪些配套能力?", "为什么说大模型只是智能体的大脑,还需要其他模块配合?"], ["怎么从零开始搭一个自己的AI智能体?", "普通人能自己动手做一个AI智能体吗?需要哪些步骤?", "搭建智能体的具体操作流程是怎样的?", "有没有适合新手的智能体搭建指南?", "搭建一个可用的AI智能体难不难?要学编程吗?"], ["给智能体设定角色时要注意什么?", "怎么让AI智能体像某个具体岗位的人一样工作?", "上传历史文档算不算角色设定的一部分?", "角色设定和知识边界分别指什么?怎么设置?", "能不能让智能体只懂我公司的业务,不回答外部问题?"], ["什么是RAG?怎么用它给智能体加‘记忆’?", "怎么让AI记住我的笔记、SOP和白皮书?", "文档切片和向量检索在智能体里起什么作用?", "挂载知识库后,智能体是不是就能回答公司内部问题了?", "RAG是不是就是把我的资料喂给AI让它记住?"], ["智能体的工作流设计具体怎么做?", "自媒体运营类智能体是怎么自动完成热点追踪→写文案→做图→分发的?", "怎么把日常重复工作变成智能体可执行的流程?", "工作流设计和普通自动化脚本有啥区别?", "一人公司怎么用工作流设计让一个智能体顶好几个岗位?"], ["AI智能体怎么才能像真人一样有手有脚去干活?", "智能体怎么接入外部工具比如搜索和数据库?", "让AI智能体具备实际操作能力,需要接哪些插件或API?", "AI智能体的‘手脚’具体指什么?怎么实现?", "智能体怎么调用外部服务来执行任务?"], ["怎么防止AI胡说八道乱编答案?", "做AI智能体时最容易踩哪些坑?", "MVP原则在AI智能体开发中怎么落地?", "怎么通过对话日志优化提示词?", "智能体上线后该定期看哪些数据来迭代?"], ["OPC的1+N组织架构到底是什么意思?", "OPC模式里‘1’和‘N’分别代表什么?", "什么是1+N架构?跟传统公司结构有啥不一样?", "OPC组织里核心层和数字员工是怎么分工的?", "1+N架构中的N是固定人数还是可伸缩的?"], ["一人公司老板每天到底该干哪四件事?", "核心层那一个人只做战略、标准、审核和管钱吗?", "定标准和审关键输出具体指哪些工作?", "现金流管理在一人公司里为什么必须由核心层亲自抓?", "一人公司的创始人是不是就只管这四件事,其他全交给AI?"], ["AI数字员工有哪些常见岗位类型?", "一个AI智能体能当销售客服,还能当财务行政吗?", "产品岗和营销岗的AI智能体是不是要分开建?", "流量投放和内容创作能用同一个AI智能体搞定吗?", "交付项目和财务行政这类不同职能,为啥要拆成独立Agent?"], ["AI搞不定的设计、剪辑、翻译这些活儿,具体怎么外包出去?", "哪些工作适合外包给外部团队而不是让AI干?", "为什么设计剪辑翻译这类重活要外包,而不是全靠AI?", "外部协作层主要包哪些活?能举几个实际例子吗?", "一人公司怎么把AI干不了的活儿交给别人做?"], ["什么叫流水线工作流?和运营闭环有啥区别?", "流水线工作流具体指哪几个环节?", "园区说的‘流水线工作流’到底是什么意思?能通俗讲讲吗?", "工作流是按步骤自动跑的吗?需要人工干预吗?", "流水线工作流和日常说的SOP是一回事吗?"], ["一人公司用AI的标准流程是怎样的?从接单到交货走几步?", "AI生成初稿后,创始人具体要审哪些地方?怎么微调?", "成品入库或上线前,AI优化排版都做些什么?", "需求输入之后,AI是怎么一步步拆解和生成的?", "标准工作流里哪几步必须人来把关?哪几步能全自动?"], ["AI客服真能7×24小时回消息还自动生成报价单?靠谱吗?", "AI怎么实现多平台批量发内容?支持哪些平台?", "标准化SOP+AI自动交付,实际是怎么落地的?", "AI记账报税能做到什么程度?需要人复核吗?", "运营闭环包含哪几块?每块AI具体干了啥?"], ["一人公司启动成本真能压到每月500块?都包含哪些开销?", "试错周期缩短到2-6周,是指从想法到验证结果只要这么短时间吗?", "相比传统创业,成本省在哪?500元和5000元档差别大吗?", "为什么AI能大幅压缩启动成本和试错时间?底层逻辑是啥?", "每月500-5000元这个成本范围,覆盖了哪些服务或工具?"], ["国内大模型厂商有哪些?怎么分梯队的?", "豆包、通义千问和DeepSeek是不是国内前三的大模型?", "国内大模型‘三超多强’具体指哪几家?", "字节、阿里、百度、腾讯各自的大模型叫什么名字?", "Kimi和MiniMax属于国内大模型的哪个梯队?"], ["大模型之间差别大吗?", "问答简单问题时,不同大模型效果差多少?", "为啥说复杂任务才看出大模型好坏?", "多模态和多轮对话能力哪家强?", "常识题大家都能答对,那真正比拼的是啥?"], ["DeepSeek适合干啥?写代码行不行?", "DeepSeek开源吗?许可证是啥?", "DeepSeek API贵不贵?性价比怎么样?", "数学和推理任务用DeepSeek好不好?", "有没有开源又便宜、还擅长代码的大模型?"], ["通义千问适合企业用吗?跟阿里云有啥关系?", "通义千问中文理解强不强?长文本处理怎么样?", "阿里云百炼平台支持通义千问吗?", "通义千问回答问题完整度高不高?", "企业上云选大模型,通义千问有啥优势?"], ["豆包能干啥?跟其他大模型比有啥特点?", "豆包免费的多模态功能具体包括哪些?", "豆包适合做智能体(Agent)开发吗?", "豆包在逻辑推理和编程方面表现如何?", "日活很高的豆包,主要面向C端还是B端用户?"], ["智谱GLM适合做编程任务吗?", "GLM模型在政企私有化部署方面表现怎么样?", "智谱的开源大模型支持长上下文吗?", "GLM做长程Agent任务效果如何?", "有没有适合私有化部署、又擅长写代码的国产大模型?"], ["百度文心一言在中文写作方面强不强?", "文心一言支持图片和语音输入吗?", "哪家国产大模型的合规资质最全?", "搜索功能强、又能写公文的国产大模型是哪个?", "文心一言是不是全模态能力最强的国产模型?"], ["Kimi能处理上百页的PDF研报吗?", "月之暗面的Kimi适合分析财报或行业报告吗?", "有没有特别擅长读长文档的国产大模型?", "Kimi在解析超长文本方面比其他国产模型强在哪?", "哪款国产模型看PDF、研报最准最细?"], ["讯飞星火在教育场景用得多吗?", "科大讯飞的大模型语音识别和合成效果怎么样?", "央国企常用哪个国产大模型做信创适配?", "星火在医疗问诊辅助方面有实际落地案例吗?", "中文理解好、还能语音交互的国产模型是哪个?"], ["混元大模型和微信打通了吗?", "腾讯混元支持图文生成和视频理解吗?", "混元有没有‘快思考’和‘慢思考’两种推理模式?", "社交场景下用得最多的国产大模型是哪个?", "哪个国产模型在微信生态里能直接调用AI能力?"], ["写代码商用推荐哪个国产大模型?", "处理超长文档用哪个国内大模型最好?", "有没有免费又好用的国产多模态大模型?", "政企项目落地该选智谱、文心一言还是通义千问?", "做AI创业项目,国内大模型怎么选型?"], ["国内各家大模型价格为什么不好直接比?", "比价时要注意哪些价格口径问题?", "美元计价和人民币计价的大模型服务怎么换算对比?", "缓存命中率会影响大模型使用单价吗?", "上下文长度不同,大模型收费会变吗?"], ["昆明大学生创业园首批入驻了多少个项目?", "目前有多少项目成功入驻园区?", "园区首期覆盖了哪些产业领域?", "人工智能、低空经济这些方向在园区有项目落地吗?", "园区第一批受理的58个项目里,最终入驻率是多少?"], ["这份知识库的数据来源是哪些文件?", "运营数据里的‘官方统计口径’和‘省级绩效累计口径’有啥区别?", "AI平台运行指标能和园区官方统计数据混着用吗?", "知识库里提到的三类数据口径分别指什么?", "《昆明市大学生创业园创业企业(项目)入驻管理规程》在知识库中起什么作用?"], ["昆明市大学生创业园的英文全称是什么?", "这个园区的正式名称叫什么?", "昆明大学生创业园是不是就叫‘昆明市大学生创业园’?", "KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK 对应的中文名是啥?", "园区全称里带不带‘市’字?"], ["云南首家政府办的大学生创业孵化园区是哪个?", "有没有免费给大学生创业用的政府园区?", "昆明有没有专为青年大学生开的创业孵化基地?", "这个园区是政府主办的吗?对大学生收费吗?", "听说有个开放式创业园区专门服务大学生,是真的吗?"], ["这个园区是哪一年成立的?", "昆明市大学生创业园什么时候开始运营的?", "2009年9月成立的创业孵化基地是哪个?", "园区创办至今有多少年了?", "最早一批大学生创业孵化载体是什么时候建的?"], ["昆明大学生创业园归哪个部门管?", "谁在负责管理这个创业孵化园区?", "昆明市人社局是不是这个园区的主管单位?", "园区的上级主管部门是人社局吗?", "这个创业园是昆明市人社局直接办的吗?"], ["昆明大学生创业园具体在哪儿?", "园区地址是昆明市民航路229号吗?", "怎么去昆明市人力资源中心1-2楼的创业园?", "在昆明市民航路附近有个创业孵化基地,是不是就是这个?", "这个园区和昆明市人力资源中心是一个地方吗?"], ["园区大概有多大面积?", "能同时容纳多少个大学生创业团队?", "3000平米的创业空间能入驻多少支队伍?", "听说最多能支持50个团队,是真的吗?", "园区的物理空间和承载能力怎么样?"], ["园区主要发展哪些产业?", "这个创业园重点支持什么类型的创业项目?", "园区的主导产业是数字经济和电子信息吗?", "你们园区聚焦在哪些行业领域?", "创业孵化主要面向数字经济和电子信息企业吗?"], ["园区是什么时候被评为省级创业园的?", "园区获得省级创业孵化载体认定是哪一年?", "2012年是不是园区被认定为省级创业园的时间?", "园区成为省级创业园有多久了?", "省级创业园资质是哪年拿到的?"], ["园区的联系电话是多少?", "我该打哪个电话咨询入园事宜?", "能发一下园区的客服电话和邮箱吗?", "联系园区工作人员用什么方式?", "园区接收咨询的邮箱是714066050@qq.com吗?"], ["园区提供哪些创业服务?", "什么是‘七位一体’创业扶持体系?", "园区说的‘一站式’孵化服务具体包括哪些内容?", "AI赋能是园区服务的一部分吗?", "空间、孵化、融资、政策这些服务都包含在园区服务体系里吗?"], ["现在园区里有多少家企业在运营?", "截至2026年7月,园区入驻企业总数是多少?", "38家是园区当前的实际入驻企业数量吗?", "最新统计的在园企业数是38家吗?", "2026年7月的数据说园区有38家企业入驻,对吗?"], ["园区最多能容纳多少家企业?", "这个园区一共能入驻几家单位?", "园区的总企业容量是多少?", "园区最多可以放几家公司?", "园区的企业承载上限是几个?"], ["园区开园到现在总共花了多少钱运营?", "到目前为止,园区运营投入了多少钱?", "园区累计花了多少运营经费?", "从开园到现在,园区运营资金总共用了多少?", "园区运营至今一共投入了多少万元?"], ["现在园区里的企业一共招了多少人就业?", "入驻企业今年加起来带动了多少人就业?", "园区企业今年提供了多少个就业岗位?", "目前园区企业累计解决了多少人的就业?", "园区企业今年一共吸纳了多少就业人员?"], ["园区企业今年一共赚了多少钱?", "入驻企业今年的生产经营总收入有多少?", "园区企业今年的总营收是多少?", "所有入驻企业今年合计做了多少营业额?", "园区企业今年生产经营总额达到多少万?"], ["园区企业今年一共交了多少税和利润?", "入驻企业今年上缴的税利总额是多少?", "园区企业今年给政府交了多少钱(含税和利润)?", "今年园区企业累计上缴税利总共多少?", "园区企业今年的税利总额有多少钱?"], ["园区开园到现在一共孵化成功并毕业的企业有多少家?", "从开园到现在,有多少家企业已经成功出园了?", "园区累计成功孵化并出园的企业总数是多少?", "截止目前,园区累计有多少家企业顺利毕业?", "开园以来,园区一共送走了多少家孵化成功的企业?"], ["今年园区一共办了多少场活动?", "2026年园区组织了多少次培训?", "今年园区开展了多少场活动,培训了多少人?", "今年园区的活动场次和培训人数分别是多少?", "2026年园区总共培训了多少人次?办了多少场活动?"], ["统计表里哪些数据是累计的、哪些是当月的、哪些是当年的?", "《2026年7月运行情况统计表》中第3栏和第9栏代表什么?第5栏呢?", "园区统计表里的数据怎么区分是累计、当月还是当年的?", "统计表中各栏数据的统计口径分别是什么?", "怎么看懂园区统计表里不同栏位的数据含义?"], ["省级创业孵化载体认定以来,园区一共孵化了多少家企业?", "截至2026年5月,园区累计孵化企业数是多少?", "园区被评上省级孵化载体后,总共孵化了多少家企业?", "按省级绩效口径算,园区累计孵化企业总数是多少?", "截至今年5月,园区在省级认定下累计孵化了多少家企业?"], ["园区到目前为止一共送走多少家团队或企业?", "省级口径下,园区累计出园的创业实体有多少家?", "2026年这一年园区有多少家团队(实体)出园了?", "截至2026年5月,园区累计和本年度的出园数分别是多少?", "园区累计出园企业和今年新出园的企业各有多少家?"], ["园区入驻团队总共申请了多少项发明专利?", "截至2026年5月,园区累计有多少件发明专利?", "今年新申请的发明专利有多少项?", "入驻团队的专利总数是多少?", "园区孵化企业一共拿了多少个发明专利?"], ["园区入驻团队到现在一共赚了多少钱?", "截至2026年5月,入驻企业的总经营收入有多少?", "累计经营收入大概多少个亿?", "园区企业总共营收多少万?", "现在所有入驻团队加起来收入有2.19亿吗?"], ["园区企业一共交了多少税?", "截至2026年5月,累计缴税金额是多少?", "入驻团队总共纳税426.2万元对吗?", "园区孵化企业合计缴了多少税款?", "税收缴纳总额接近430万了吗?"], ["园区目前带动了多少人就业?", "累计带动就业人数有多少?", "今年新增带动就业多少人?", "1529人是包括今年新招的106人吗?", "园区总共帮多少人找到了工作?"], ["园区创业指导专家一共有多少人?", "运营团队有几个人?", "专家团队和运营人员各有多少?", "创业导师队伍规模多大?", "园区有50个专家加3个运营人员对吗?"], ["园区一共办过多少场创业指导服务活动?", "截至2026年5月,园区累计开展了多少场创业指导活动?", "今年(2026年)园区办了多少场创业指导活动?", "创业指导服务活动总共83场,其中今年有几场?", "园区创业指导活动历年累计和本年度分别是多少场?"], ["园区近3年孵化成功的创业企业比例是多少?", "入孵企业的孵化成功率有多少?", "过去三年里,园区孵化成功的比例是95.91%吗?", "孵化成功率95.91%是指最近三年的吗?", "园区近三年的孵化成功率具体是多少?"], ["哪些人可以申请入驻园区?", "在校大学生能申请入驻吗?毕业几年内的毕业生可以?", "留学生包括归国和外籍的都能申请吗?", "不是法定代表人,只是项目负责人能入驻吗?", "如果公司很优质但申请人不是法人,还能放宽入驻吗?"], ["园区重点支持哪些产业方向的创业项目?", "昆明市‘10+N’产业链具体包括哪些?", "做数字经济或生物医药的项目符合入驻产业要求吗?", "绿色食品、文化旅游这些算园区支持的产业吗?", "我的项目属于新能源领域,符不符合园区的产业定位?"], ["入驻园区对企业经营计划和资金有什么要求?", "申请入驻需要提供详细的经营计划书吗?", "启动资金有没有最低金额要求?", "企业管理制度要规范到什么程度?", "有知识产权纠纷或者经营异常记录还能申请入驻吗?"], ["哪些创业项目能优先入驻园区?", "拿过市级以上创业比赛奖的项目是不是更容易进园区?", "有发明专利或者高学历的团队,入驻园区有没有优待?", "学术成果特别突出的创业者,申请园区入驻会加分吗?", "园区对拥有高新技术专利的项目,入驻时有什么特殊政策?"], ["园区一年组织几次入驻申请?", "入驻申请是固定时间还是随时都能提交?", "如果错过了常规申请时间,还能不能补报?", "园区入驻工作每年安排几次?会不会临时加开?", "入驻申请的时间安排是固定的吗?能不能根据情况调整?"], ["申请入驻园区需要准备哪些材料?", "第一次申请园区入驻,要交哪些表格和证明?", "还没注册公司,只有一份创业计划书,能申请入驻吗?", "外籍创业者申请园区入驻,身份证件要提供什么?", "提交入驻申请时,专利证书和学历证这些必须一起交吗?"], ["材料不全的话,园区会怎么处理我的入驻申请?", "初审没通过是因为材料少了,还能补交吗?", "服务机构初审时发现材料缺东西,会告诉我补什么吗?", "提交材料后被告知不齐全,是不是还有机会补正?", "初审阶段材料有问题,是一次性告知还是分批提醒?"], ["入驻评审是怎么进行的?要答辩吗?", "路演答辩具体怎么操作?有没有评分标准?", "评审专家从哪里来?几个人打分?多少分算通过?", "入驻评审总分100分,80分以上才合格,那各项分别占多少分?", "团队实力、项目创新性、市场前景这些在评审里各占多少比重?"], ["公示是在哪个网站上进行的?", "公示完没异议是不是就代表能入驻了?", "公示阶段需要我本人做什么吗?", "公示期是多长时间?如果有人提异议怎么办?", "公示结果在哪里查?人社局官网具体哪个栏目?"], ["签完协议后多久能拿到工位?", "《入驻孵化协议》里都写了哪些关键内容?", "签协议的时候要带什么材料?", "办公场地是随机分配还是可以自己选?", "签了协议但还没拿到工位,算不算正式入驻?"], ["OPC创业空间适合做AI小程序的团队吗?", "做电商直播的项目能申请OPC的独立工位吗?", "文创设计类项目在OPC最多能待几年?", "OPC是直接就能搬进去办公,不用装修吧?", "软件开发和AI应用类项目,OPC和其他区域有啥区别?"], ["还没注册公司,但有个创业点子,能进园区吗?", "大学生创业团队只能进创业苗圃区吗?", "在苗圃区表现好,怎么才能去加速区?", "创业苗圃区给的是不是那种开放大办公室?", "苗圃区最多待两年,之后必须搬走吗?有没有其他出路?"], ["已经注册了公司,但刚成立三个月,能进孵化加速区吗?", "加速区的2年孵化期满了还能续吗?怎么申请延期?", "独立办公空间是带家具和网络的吗?", "加速区对企业有没有营收或人员规模要求?", "总共最多能在加速区待三年,这三年里要每年评估吗?"], ["国际创客区是给哪些人用的?", "留学生创业能进国际创客区吗?", "外籍学生创业有专门的孵化区域吗?", "归国留学生创业有啥特别支持?", "国际创客区的孵化时间是多久?"], ["园区给创业团队提供免费办公场地吗?", "工位和独立办公室都免费吗?", "办公桌椅、网络这些基础设备园区包吗?", "新入驻团队能拿到哪些硬件支持?", "有没有免费的高速网络和文件柜?"], ["创业初期有老师一对一指导吗?", "园区会组织创业相关的活动吗?", "有没有创业沙龙或者路演机会?", "创业讲堂多久办一次?参加要收费吗?", "刚起步的项目能获得哪些辅导服务?"], ["园区能帮忙找投资吗?", "想贷款创业,园区能推荐银行或政策性贷款吗?", "股权融资的事园区管不管?", "有没有对接风投机构的服务?", "融资这块园区具体怎么帮创业者?"], ["园区能帮我们申请创业补贴吗?", "税收减免和社保补贴怎么弄?", "高校毕业生创业有哪些专属政策?", "创业扶持资金申报流程园区会协助吗?", "政策对接服务包括哪些具体内容?"], ["园区能帮我们对接哪些高校和科研资源?", "能不能推荐我们参加春城创业荟这类活动?", "园区怎么帮企业融入昆明的创新创业服务体系?", "入驻后能对接昆明本地龙头企业吗?", "园区有没有和驻昆高校合作的资源?"], ["园区提供哪些免费的AI培训课程?", "有没有教怎么用AI做内容创作的培训?", "想学AI怎么帮公司省钱增效,园区有相关培训吗?", "一个人创业,园区有专门的AI落地指导吗?", "AI公益培训都包括哪些具体内容?"], ["园区能帮忙办营业执照吗?", "税务申报这些事园区能代办吗?", "知识产权方面有问题,园区能给咨询建议吗?", "遇到法律纠纷,园区提供法律援助吗?", "工商、税务、法务、知产这些基础服务园区都包办吗?"], ["园区每年怎么考核入驻企业?", "考核满分多少分?多少分算合格?", "考核主要看哪几个方面?各自占多少分?", "经营发展能力和技术创新能力分别占多少分?", "考核等级分几档?每档的分数线是多少?"], ["企业什么情况下要离开园区?", "孵化成功了是不是就得搬走?", "如果自己不想继续入驻了能主动退出吗?", "什么情况会被园区强制要求退出?", "孵化期满了还没毕业,还能继续待吗?"], ["如果公司连续好几天没人来办公室办公,会不会被园区赶出去?", "场地空着没用又没提前报备,几次会被强制清退?", "什么情况下园区会直接让企业搬走?比如欠钱、违法或者乱转租行不行?", "不服从园区管理到什么程度算严重?会被踢出去吗?", "公司执照被吊销了,是不是必须马上退出园区?"], ["啥叫OPC创业模式?是不是一个人就能开公司?", "AI一人公司到底是什么意思?靠人工智能怎么单干创业?", "做电商直播或者AI应用,一个人能注册公司并在园区起步吗?", "OPC适合哪些行业?搞软件开发或文创设计能行吗?", "轻资产创业是不是就指不用租大办公室、一个人就能干的活?"], ["OPC创业者在园区能租到独立工位吗?最长能待多久?", "园区给一人公司提供共享办公位吗?有没有拎包入住那种?", "孵化期最多两年,是从签合同那天开始算,还是入驻当天起算?", "园区有没有专门教一个人怎么用AI创业的培训课?叫啥名字?", "‘AI一人公司实战课’多久开一次?新人能报名参加吗?"], ["云南第一个AI一人公司基地在哪?什么时候建好的?", "昆明是不是全省首个搞AI个体创业培育的地方?有官方消息吗?", "2026年3月说的云南首个人工智能OPC人才基地,具体在昆明哪个位置?", "这个AI OPC基地是政府牵头还是园区自己建的?", "之前云南没有专门培养AI个体创业者的基地,对吗?"], ["现在园区里到底有多少家企业?38家还是39家?为啥数字不一样?", "《运行情况统计表》和《入驻企业信息表》哪个才是最新最准的名单?", "云南派音、智海银高、廷秀文旅这些公司都在园区里办公吗?", "名单里有‘人工智能机器人大模型训练’这种项目,是真实入驻的企业吗?", "中越、滇缅、仰光客厅这些带国际字样的项目,也是园区的正式入驻单位?"], ["园区里有哪些企业?能按行业分类列出来吗?", "达岸教育、昆明滇油石化这些公司都在园区里吗?它们分别属于什么行业?", "人工智能和跨境电商类的企业在园区里各有多少家?", "园区里做文旅康养和生物医药的企业有哪些?", "云南星瑞航空和昆明云韵体育是不是也在这个园区?它们属于哪个行业类别?"], ["低空经济相关的企业在园区有几家?", "园区里搞现代农业的公司有哪几个?", "绿色环保和新材料方向的企业各有多少家?", "企业服务类的企业在园区有几家?都叫什么名字?", "低空经济、现代农业、绿色环保这些新兴领域在园区的布局情况是怎样的?"], ["AI平台每个月大概消耗多少token?", "平台每天平均用多少token?每秒处理量大概是多少?", "200亿tokens每月这个数据是官方统计的还是内部测算的?", "AI平台的token消耗量是怎么算出来的?有没有日均或实时速率参考?", "6.67亿tokens日均和7700t/s实时速率这两个数字准确吗?来源是哪里?"], ["每次调用工具大概消耗多少token?", "工具调用次数和token消耗之间是怎么换算的?", "15万tokens/次这个平均值是所有工具都一样吗?", "工具调用和token消耗是实时联动的,具体怎么联动的?", "如果调用次数变多了,token消耗会同比例增加吗?"], ["怎么查园区最新的实时运营数据?", "get_park_overview这个工具要怎么调用?在哪里用?", "园区的实时数据以什么为准?是网页显示的还是必须调工具?", "调用get_park_overview能拿到哪些具体指标?", "实时运营数据更新频率是多少?调一次能获取多长时间范围的数据?"], ["我想申请入驻园区,具体要准备哪些材料?流程是怎样的?需要答辩吗?多少分才算通过?", "入驻昆明市大学生创业园要走什么流程?提交材料后多久能知道结果?每年有几次申请机会?", "创业项目怎么才能进园区?是不是要先交计划书和身份证?评审环节是几个人打分?最低多少分能过?", "园区入驻要面试或路演吗?评审专家从哪来?公示是在哪里看?签协议前还要做什么?", "新人怎么申请入园?需要学历证明吗?评审时是线上还是线下?通过标准是80分固定分吗?"], ["园区给创业公司提供哪些免费或补贴类支持?比如办公室、培训、找投资这些有没有?", "在园区创业能享受哪些实际帮助?像办公场地、法律咨询、AI培训这些包不包括?", "听说有‘一站式’服务,具体包含工商、税务、法律哪些内容?AI专项培训是定期开班吗?", "除了免租金,园区还帮企业对接融资、申报政策吗?辅导是免费的吗?", "园区有哪些孵化服务?空间免费是真的吗?政策申报是指帮我们申请政府补贴吗?"], ["现在园区里有多少家在孵企业?总共带动了多少人就业?是按年度算还是累计算?", "2026年7月的数据说有38家企业,那带动就业213人是指这38家今年新招的吗?1529人又是怎么算出来的?", "园区就业数据有两个口径,到底该用哪个?比如写汇报材料时该引用哪个数字?", "‘累计带动就业1529人’是截至什么时候的?经营收入2.19亿是所有企业的总和吗?", "园区目前实际在运营的企业数量是多少?最新就业带动人数怎么看?不同统计口径的区别在哪?"], ["OPC是什么意思?是不是就是一个人开的公司?和普通个体户有啥区别?", "听说园区支持OPC创业模式,这个‘一人公司’具体指什么?需要几个人注册?", "OPC是昆明园区特有的吗?和有限责任公司比,注册和运营上有什么不同?", "想一个人创业,能注册OPC吗?园区对OPC有没有额外扶持?", "‘One Person Company’翻译成中文就是一人公司吗?在园区里怎么申请这种类型?"], ["园区的原始资料和数据都存在哪里?比如入驻企业名单、运行统计表这些文件能公开查吗?", "对外申报或写总结报告时,该参考哪些权威文件?比如Excel表格和PDF宣传册哪个更准?", "‘数据资料/’目录里的几个文件,比如入驻企业信息表和运行情况统计表,最新更新到什么时候?", "品牌设计稿和空间布局图这些视觉材料,能用于对外宣传吗?有没有使用规范?", "园区所有原始材料都在‘数据资料/’文件夹里吗?比如管理规程和宣传册这些,是不是最新定稿版本?"], ["宣传推广可以用这个知识库里的内容吗?", "我能不能拿这里面的信息去做园区宣传?", "知识库里哪些内容适合对外宣传用?", "做推广材料时能直接引用这里的资料吗?", "宣传推广相关的工作可以参考这个知识库吗?"], ["园区的基本情况都在哪查?", "我想了解园区的基本信息,该看哪部分?", "园区基本信息包括哪些内容?在目录里怎么找?", "哪里能找到园区的概况、地址、面积这些基础资料?", "目录里标着‘园区基本信息’的是第几项?"], ["运营绩效的核心指标有哪些?", "省级申报要求的运营绩效指标是哪些?", "申报省级园区时要看哪些关键绩效数据?", "运营绩效指标按省级标准是怎么定的?", "目录里‘运营绩效核心指标(省级申报口径)’具体指什么?"], ["2026年1到7月的运行数据有吗?", "每月的运行统计表在哪找?", "有没有2026年前七个月的月度运行情况汇总?", "运行情况的月度统计更新到几月份了?", "目录里说的‘2026年1—7月运行情况月统计’包含哪些内容?"], ["入驻企业名单在哪里看?", "一共有多少家企业入驻园区?", "能查到所有入驻企业的详细信息吗?", "39家入驻企业的信息表怎么获取?", "目录里‘入驻企业完整名录(39家/信息表)’包含联系方式和业务范围吗?"], ["孵化加速区在哪儿?", "加速1到10号房间属于哪个区域?", "园区里叫‘孵化加速区’的地方是哪一块?", "我想去孵化加速区,怎么找?", "加速1–10这些办公室是不是都在孵化加速区?"], ["国际创客区具体在什么位置?", "国际1到7号房间属于哪个功能区?", "园区里有国际创客区吗?在哪?", "国际创客区是不是专门给外国创业者用的?", "国际1–7这些工位是在国际创客区吗?"], ["成长区包括哪些房间号?", "成长1到8号房间归哪个区域管?", "园区的成长区在几号楼?怎么走?", "创业公司发展到一定阶段能进成长区吗?", "成长1–8这些空间是不是叫成长区?"], ["OPC创业空间是独立办公区吗?", "OPC创业空间有没有固定房间号?", "园区里提到的OPC创业空间是指哪个地方?", "OPC创业空间和别的区域比如孵化加速区是一样的吗?", "OPC创业空间是对外开放申请的吗?"], ["一楼直播间在园区哪一层?", "园区的一楼直播间具体在哪个位置?", "想用直播间得去一楼吗?", "E区就是一楼直播间对吧?", "一楼直播间是编号E吗?开放给谁用?"], ["园区里有哪些已经成功入驻的省级申报企业?", "能给我看看省级申报的31家入驻实体名单吗?", "哪些企业是通过省级申报进园区的?", "省级申报入驻的企业一共有多少家?", "有没有省级申报入驻企业的详细名单?"], ["企业入驻园区需要遵守哪些管理规定?", "园区对企业(项目)入驻有哪些管理要求?", "入驻后园区怎么管理企业和项目?", "企业入驻管理规程主要讲了哪些内容?", "园区对企业入驻后的日常管理有哪些要点?"], ["园区的整体空间是怎么规划的?", "园区内部的空间布局是怎样的?", "能介绍一下园区各功能区域的分布情况吗?", "园区有哪些主要的功能分区?", "园区的物理空间结构或场地划分是怎样的?"], ["园区宣传册里都写了些什么内容?", "宣传册里关于园区简介、服务和政策的部分具体包括哪些?", "想快速了解园区,宣传册的核心信息有哪些?", "园区宣传册重点介绍了哪些方面?", "宣传册里关于服务和扶持政策的内容有哪些?"], ["企业要满足什么条件才能入驻园区?", "新企业入驻园区的具体流程是怎样的?", "从申请到正式入驻园区,一共要走哪些步骤?", "入驻园区需要符合哪些硬性条件?", "入园的门槛和办理流程分别是什么?"], ["孵化周期是多久?", "园区有哪几个孵化区域?", "四大孵化区域分别叫什么名字?", "不同孵化区域的周期一样吗?", "孵化时间最长能有多久?"], ["怎么报名入驻园区?", "报名需要走什么流程?", "申请入园要提交哪些材料?", "线上还是线下报名?", "报名入口在哪里?"], ["园区上过哪些媒体报道?", "有哪些媒体和园区合作过?", "园区被哪些新闻平台报道过?", "合作媒体名单能看看吗?", "有没有主流媒体对园区的采访或专题?"], ["园区最近有什么新闻?", "园区举办过哪些活动?", "有没有园区的年度大事记?", "想了解园区近期动态,从哪看?", "园区过去一年都做了哪些重要事情?"], ["园区的品牌设计规范在哪查?", "VI手册包含哪些内容?", "Logo、字体、颜色这些怎么用才合规?", "品牌视觉识别标准有官方文件吗?", "对外宣传时VI使用要注意什么?"], ["昆明市大学生创业园是省级的吗?什么时候评上的?", "这个园区叫什么名字?在昆明哪个位置啊?", "园区是哪年成立的?主管部门是谁?谁在负责运营?", "听说2026年4月园区焕新亮相了,是真的吗?现在归谁管?", "园区的运营公司叫啥?统一社会信用代码是多少?申报单位负责人电话能给一下吗?"], ["园区主要支持哪些行业的创业项目?", "整个园区有多大?办公场地和孵化场所各占多少面积?", "园区总共投了多少钱?现在资产值多少?最多能容纳多少个创业团队?", "为什么有的地方写能容纳50户,有的又说49个?哪个数准?", "园区拿过哪些国家级或省级的牌子?比如国家众创空间、科技企业孵化器这些算不算?"], ["园区简介里说在民航路301号,但地址又写229号,到底哪个对?", "园区给大学生创业提供哪些免费服务?除了场地还有啥?", "园区的服务理念和办园宗旨是怎么说的?", "到2026年5月为止,一共孵化了多少家企业?现在还在孵的有多少?", "园区带动了多少人就业?帮企业拿到了多少创业扶持资金?"], ["目前园区实际入驻了多少家创业团队?今年新进了几家?", "应届毕业生(含离校2年内)在园区创业的有多少家?今年新增了几家?", "园区里的创业团队一共拿了几个发明专利?今年新申请了多少项?", "入驻团队去年一共赚了多少钱?交了多少税?今年缴了多少?", "园区创业指导专家有多少人?今年组织了多少场创业指导活动?"], ["园区孵化成功率和出园率分别是多少?数据是近3年的吗?", "现在孵化场所的使用率有多高?90%是指什么?", "返乡农民工能在园区创业吗?有没有相关扶持?", "园区累计孵化181家,但表格里‘团队出园数’写150户,差的那31家去哪了?", "运营团队现在有几个人?是不是就3个工作人员管整个园区?"], ["昆明市大学生创业园的运行情况统计表是哪份文件?", "2026年7月的昆明创业园数据出自哪个表格?", "《昆明市创业平台运行情况月统计表》和《年度汇总表》是不是同一份材料?", "这份园区数据是来自Excel文件还是其他格式?文件名是什么?", "‘昆明市大学生创业园运行情况统计表(2026年7月).xls’这个文件包含哪些内容?"], ["昆明大学生创业园现在有多大?建筑面积是多少?", "园区总共投了多少钱运营?累计花了多少?", "园区最多能容纳多少家企业?现在实际有多少家在园里?", "这些企业一共招了多少人就业?总共赚了多少钱、交了多少税?", "开园到现在一共孵化成功、毕业出园的企业有多少家?今年办了多少场活动、培训了多少人?"], ["报表里哪些数字是‘从开园到现在’一直累计的?哪些是‘今年1到7月’的?", "‘实际入驻企业数’是当月的还是全年的?怎么区分累计值和当期值?", "表格第3栏和第9栏为什么标着‘累计’,其他栏又说是‘当年累计’?有啥区别?", "‘报送当月实际入驻企业数’具体指哪一栏?怎么查这个月新进或退出的企业?", "为什么有些指标写‘当年累计’,有些写‘开园以来累计’?怎么看懂这个统计口径?"], ["园区4月份搬去民航路229号后,面积、容量和资金都变了,具体怎么变的?", "从1月到7月,入驻企业数、带动就业人数这些数据是怎么一步步增长的?", "3月到4月之间园区发生了什么变化?为什么可容纳数突然从18变成49?", "7月底的数据和年初相比,哪些指标增长最多?比如就业人数、营收、活动场次?", "年度汇总表里每月的数值变化能反映园区发展节奏吗?比如哪个月扩张最快?"], ["园区负责人和填表人是谁?联系方式能发我一下吗?", "想联系园区对接工作,找李云鹏还是李禹贤?电话和邮箱是多少?", "填表时间是2026年7月31日,这个数据是最新的吗?还有效吗?", "单位负责人李云鹏和填表人李禹贤是什么关系?分别负责什么?", "714066050@qq.com这个邮箱能用来咨询入园政策吗?联系电话18087174891是24小时开通的吗?"], ["昆明大学生创业园有哪些分区?", "创业园的场地安排分哪几个区?", "孵化加速区、国际创客区这些分区是怎么划分的?", "OPC创业空间和一楼直播间是不是园区的正式分区?", "园区里说的‘加速’‘国际’‘成长’‘OPC’‘一楼直播间’分别指什么区域?"], ["孵化加速区一共有几家入驻企业?", "加速1到加速8,还有加速10,为什么没有加速9?", "孵化加速区总共多少家企业?有没有漏掉编号?", "加速区的企业数量是9家吗?都包括哪些编号?", "创业园孵化加速区包含哪些编号的企业?"], ["加速1公司叫什么名字?负责人是谁?毕业学校和专业是什么?", "云南派音人工智能科技有限公司的创始人王思松是哪个学校毕业的?", "派音科技什么时候启动和注册的?招了几个人?", "派音科技做的是什么业务?他们的Pine模型系列有什么功能?", "加速1公司主要研究什么技术?音频识别、音效生成这些是不是他们做的?"], ["加速2项目叫什么?负责人王昱博是哪所学校毕业的?", "米勒克尔蓝宝石珠宝项目是海归做的吗?学的是什么专业?", "这个蓝宝石项目具体做哪些事?晶体生长和珠宝设计都包括吗?", "蓝宝石新材料和珠宝产业怎么融合?跟上下游怎么合作?", "加速2项目主打高附加值蓝宝石珠宝,是真的做实物珠宝还是只做材料?"], ["加速3项目名称是什么?负责人刘泽位在读什么博士?", "中泰研学合作项目和泰国西那瓦大学有什么关系?", "这个项目提供出国留学服务吗?还做校企合作和企业出海吗?", "刘泽位会说泰语吗?他在侨联有职务吗?", "中泰研学合作项目覆盖哪些服务?比如中外合作办学、产教融合这些包不包括?"], ["云南宸中低空经济有限公司是干啥的?负责人是谁?什么时候成立的?", "王爱玲是哪家公司的负责人?她现在在读什么学校?这家公司主要做低空经济哪些业务?", "园区里新成立的低空经济公司叫什么名字?成立时间、负责人背景和业务方向能说说吗?", "宸中低空经济是不是2026年2月刚成立的?它在园区享受哪些创业扶持政策?", "云南有没有专注低空经济全产业链的初创企业?叫什么?谁负责?有什么特色?"], ["昆明智海银高文化科技有限公司是做什么的?负责人宋芃燃是哪个学校的?", "这家公司做AI财税服务吗?都提供哪些具体服务,比如代理记账、出海咨询这些有吗?", "智海银高是2026年4月成立的对吧?它用AIGC技术做了个什么平台?", "宋芃燃是中共党员吗?她在昆明文理学院学的是审计学?公司主要服务对象是哪些企业?", "有没有做数字化财税+企业综合咨询的公司?叫什么?覆盖海外财税和财务培训吗?"], ["人工智能机器人大模型训练项目是哪个团队做的?负责人周凯什么学历背景?", "这个大模型训练项目是2026年4月启动的吗?具体做数据采集、标注还是模型优化?", "周凯是天津大学毕业的?他带队做的机器人项目主要解决哪些落地场景问题?", "园区里有没有专门做机器人大模型‘训练-优化-应用’全流程的技术项目?叫什么?", "大模型训练项目包括数据清洗和迭代优化吗?是否面向多行业场景落地?负责人是谁?"], ["云南廷秀文旅康养服务项目什么时候开始运营?负责人李雨桐是哪国留学回来的?", "李雨桐在日本读的老年社会学硕士?她回国后要做的康养项目主要解决民宿和旅居点哪些问题?", "廷秀项目针对银发人群,具体怎么提升服务标准、适老能力和运营转化率?", "云南现在推‘旅居云南’,有没有专门做老年文旅旅居服务支持体系的创业项目?", "这个康养服务项目5月启动吗?聚焦旅居空间、民宿和康养点位的服务短板,对吗?"], ["瀚颖AI+教育信息咨询项目是哪家机构发起的?负责人王翰啸是哪个学校毕业的?", "这个项目是不是2026年4月成立的?它搞AI+教育,具体做课程研发、师资共建还是企业赋能?", "王翰啸是云南大学滇池学院毕业的?他们打造的数字教育生态包含哪些环节?", "有没有融合AI技术和教育教学体系的教育服务项目?在昆明园区叫什么名字?", "瀚颖的AI+教育项目提供人才培育和企业赋能服务吗?属于全链条数字化教育生态吗?"], ["云南大学AI+联合创业服务平台是干什么的?", "顾耘华老师负责的那个AI创业平台主要做哪些AI方向?比如AI短视频、AI漫剧这些有具体案例吗?", "云南大学的AI创业平台帮大学生创业成功过吗?有没有拿国家级奖项的项目?", "育笋计划是什么?是不是云南AIGC产业联盟搞的?覆盖了多少大学生?", "王升雅、王洋、杨沐晨、王润曦他们各自做了什么AI相关项目?分别有什么成果?"], ["国际创客区一共有几家?都叫什么名字?", "园区里的国际1到国际7是哪几个国家的团队在做?", "国际创客区的位置在哪?里面都是外国留学生开的公司吗?", "国际创客区总共多少个空间?每个空间对应一个项目吗?", "国际1到国际7是按什么顺序编号的?有没有空着没入驻的?"], ["仰光客厅是谁创办的?是不是缅甸留学生做的?", "仰光客厅这个项目主要是面向谁的?中国游客、在缅中资企业还是缅甸本地年轻人?", "仰光客厅除了住宿和吃饭,还能体验什么缅甸文化?跟当地手艺人怎么合作的?", "仰光客厅是不是在缅甸仰光郊区搞了个沉浸式营地?具体做什么活动?", "仰光客厅算不算中缅文化交流平台?有没有实际带动当地人就业增收?"], ["千楣之路卖的是不是缅甸传统护肤品檀娜卡?", "檀娜卡是用什么做的?听说缅甸2025年要申非遗,是真的吗?", "千楣之路的产品分哪几类?原木套装、粉泥和现代美妆线都有吗?", "千楣之路怎么把缅甸檀娜卡卖到中国?靠小红书还是跨境电商?", "千楣之路是哪个学校的缅甸留学生做的?是不是云大工商管理学院的?"], ["联翼航空技术这个项目是做什么低空经济业务的?", "杨俊蓝是哪个学校毕业的?现在在哪儿读研?项目什么时候启动的?", "联翼航空技术主要面向东南亚哪些国家拓展?做无人机应用还是数据服务?", "低空经济培训是教外国人飞无人机吗?还是也教国内学员?", "联翼航空技术怎么利用云南的区位优势做国际化?有没有具体输出到东南亚的案例?"], ["酷享野农AI农业这个项目是哪个学校的学生搞的?负责人叫啥名字?", "晏和波是谁?他在西南林业大学读什么专业?", "酷享野农AI农业项目什么时候开始的?主要干些啥?比如用AI做农业宣传吗?", "这个农业项目是不是在种高原早熟苹果?跟科研机构和地方政府怎么合作的?", "智慧农业、农旅融合、科研种植这些词具体指啥?酷享野农项目怎么把它们结合起来?"], ["滇缅国际设计是做什么的?是不是专门帮云南和缅甸那边做工程设计?", "张浩鹏是哪个学校毕业的?学的什么专业?", "这个设计公司啥时候成立的?一带一路跟他们有啥关系?", "他们说的跨国协同设计模式是啥意思?绿色建造技术用在哪些项目上?", "交通和城乡基建类的设计,他们有没有跟国外机构一起落地过实际项目?"], ["昆明舒诺生物科技是干啥的?是不是做宠物食品或者营养品的?", "舒岐涛是哪个学校的学生?学的什么专业?", "这家公司什么时候成立的?主打‘精准营养’‘全生命周期养护’具体指什么?", "清洁标签、生物活性技术这些词听起来挺专业的,他们产品到底有啥特别?", "舒诺生物的产品是不是针对猫狗不同年龄阶段做的?配方科学在哪?"], ["达岸教育是做德国留学服务的吗?能办双元制吗?", "吕琳是在泰国读的MBA?她现在负责达岸教育的哪些业务?", "德国双元制留学包括德语培训、签证、境外陪护这些服务吗?", "达岸教育从哪年开始做这事儿的?跟哪些德国学校有合作?", "通过他们去德国留学能不能边学边赚工资?最后能拿到学历和工作机会吗?"], ["成长区一共有几家创业公司?编号是从成长1到成长8吗?", "成长区和其他区域(比如国际4、5)有啥区别?是按发展阶段分的吗?", "成长1到成长8这些项目都在同一个地方办公吗?属于园区什么类型的企业?", "成长区的企业是不是都处于初创或快速成长阶段?有没有统一的支持政策?", "我想了解成长区某一家公司,比如成长3,该怎么查它的详细信息?"], ["花仙子园艺肥料公司是哪个学校毕业的人创办的?创始人叫什么名字?", "这家做家庭园艺肥的公司是哪年成立的?主要解决养花人哪些实际问题?", "花仙子肥料有没有异味?产品是不是容易操作?现在是批量生产了吗?", "翁淞耀是云南师大哪个专业的?他做的肥料线上线下都怎么卖?", "花仙子公司服务对象主要是谁?比如阳台种菜、家里养花的人能用吗?"], ["研X平台是给哪些人用的?说的‘1621万硕博’和‘6000万泛科研人群’具体指什么?", "同行者网络这个APP有苹果和安卓版吗?高校老师有没有投资?投了多少?", "‘1+6+N’生态里的6大模块都包括啥?比如代码共享、找工作、谈恋爱这些真都有?", "研X平台现在有多少学校在用?是学生自己注册还是学校统一组织?", "陈品祥是哪个学校毕业的?现在还在教书吗?在哪儿任教?"], ["朵哈玫瑰项目是谁发起的?大学生创业能做玫瑰米酿吗?", "八街的重瓣红玫瑰是地理标志产品吗?他们跟农业龙头企业怎么合作的?", "玫瑰米酿是饮料还是酒?生产是标准化的吗?品质怎么保证?", "刘志贤是哪个学校哪个年级的学生?项目是2026年4月才开始的吗?", "朵哈项目只做玫瑰米酿,还是也会开发其他玫瑰相关产品?"], ["舒护安养老公司是做什么护理服务的?比如陪病人去医院或者上门照看老人行不行?", "严宇佳是学广播电视编导的,怎么转行做医疗护理?公司是个人开的吗?", "这家公司才孵化一个月就招了3个人?主要服务哪些病人群体?比如术后、偏瘫、慢病老人?", "舒护安跟医院有合作吗?是直接对接医院安排陪护,还是自己接单?", "居家上门护理包括生活监护吗?比如帮独居老人买菜、提醒吃药这些算不算?"], ["南菌优培项目什么时候正式注册公司?负责人王荣飞是学医的,怎么搞食用菌的?", "他们研发的生物制剂加了海藻糖-6-磷酸和寡糖?这些成分是天然的吗?", "硬皮地星和硬皮马勃是云南特有的野生菌吗?高值化全链开发具体指哪几个环节?", "团队里有分析化学博士和农业硕士,这个项目已经有三年研发基础了?", "南菌优培是专注做生物制剂,还是也做野生菌的种植、加工、销售整条链?"], ["丽裳文化艺术工作室是干啥的?主要做传统文化和茶文化活动策划吗?", "王娟老师开的丽裳工作室什么时候成立的?是不是专门帮云南特产比如茶和干花出海?", "五华区丽裳文化艺术工作室的负责人是谁?她学的是幼教专业,怎么转行做国风文旅策划了?", "丽裳工作室说搞‘账号引流+内容传播+产品出海’,具体是怎么操作的?有做抖音或小红书推广吗?", "这个工作室是不是靠云南自贸区政策优势,帮民俗文创和高原特色产品卖到国外?"], ["星瑞航空这个项目是真搞飞机还是只用无人机?他们用的是大疆哪款行业机?", "尹永浩团队能做文物数字化保护吗?除了拍照片建模,还做三维重建和数字孪生吗?", "云南星瑞航空接不接电力巡检、光伏板检测或者森林防火巡查这类活儿?夜间红外搜救也行吗?", "他们除了飞无人机,还卖设备、处理数据、培训飞手吗?是一条龙服务吗?", "星瑞航空是2026年4月刚启动的?负责人尹永浩是学数字媒体技术的,怎么切入低空智能应用领域的?"], ["那个云南直播私域服务平台叫啥名?是不是专门帮本地商家做抖音直播和私域引流的?", "金满满刚毕业就创业做直播平台?她学的是学前教育,怎么转行搞短视频创作和全域运营?", "这个平台说打造自主IP形象,还建全国性私域流量池,具体指微信社群、小程序还是APP?", "他们真在带大学生创业孵化?有没有实习、培训或者人才推荐机制?", "综合性云南直播私域服务平台是不是主要挖云南特产、非遗、旅游这些本土资源来拍短视频和开直播?"], ["OPC创业空间在哪?里面总共安排了几家单位?都叫D开头的吗?", "D. OPC创业空间是不是就是指那6家入驻企业共用的办公场地?名字里的OPC是缩写吗?", "OPC创业空间里都有哪些项目?是不是包括中外研学、直播平台这些?", "想租OPC的空间办公,得找谁申请?有没有工位或独立办公室可选?", "OPC创业空间和园区其他孵化场地有啥不一样?是不是专供成长系列项目用的?"], ["中外青少年研学项目跟哪些国家合作?英美澳新加这些是不是都有直签学校?", "王得荣老师有剑桥交流经历,这个研学项目是不是主打高端国际游学?教师和家长也能报名吗?", "这个研学服务是一站式搞定签证、课程、住宿和保险,还是只负责对接海外机构?", "他们跟国外教育机构是直营合作,不是通过中介?有没有合作院校名单可以看?", "D1项目除了青少年游学,还做在职教师海外研修或亲子家庭出境教育旅行吗?"], ["星杭商务信息咨询昆明有限责任公司是干啥的?负责人是谁?学什么专业的?", "D2那家公司主要做哪些业务?杨之华是负责人吗?她是不是云南师范大学毕业的?", "杨之华创办的星杭商务咨询公司提供培训和商务接待服务吗?她什么时候毕业的?", "星杭商务这家公司是教育学背景的人开的吗?都做哪些商务服务和信息咨询?", "D2园区企业‘星杭商务’的负责人学历和主营业务分别是什么?"], ["蓝智科技是做什么的?徐安厅是负责人吗?他哪年毕业?", "D3蓝智科技项目是不是搞农业物联网的?专门服务蓝莓种植?有自研监控系统吗?", "蓝智科技的智慧种植系统能监测田间环境和水肥灌溉吗?团队几个人?什么时候启动的?", "徐安厅带的蓝智科技是不是西南地区做蓝莓数字化种植的?他们自己开发了全系列监控系统?", "蓝智科技这个OPC公司是2026年5月开始做的吗?专注农业物联网和智慧基地运营?"], ["云品出滇·纸享万家是卖抽纸的吗?王帅是负责人?哪个学校毕业的?", "D4这个项目是不是用AI工具做电商抽纸品牌?现在在拼多多卖得怎么样?", "‘纸享万家’是不是主打悬挂式抽纸?未来要往东南亚做跨境电商?", "王帅的云品出滇项目是2026年3月成立的吗?有没有真实销售验证?", "云品出滇这个日用纸品品牌靠AI和电商平台双轮驱动?核心产品是新型悬挂抽纸?"], ["云迹轻旅是做什么文旅服务的?王海璇有十年经验吗?是不是云南大学MBA毕业的?", "D5云迹轻旅工作室提供私人定制旅行和非遗研学小团吗?也帮本地商家做文旅策划?", "云迹轻旅是不是‘内容+服务+技术’三块都做?包括自媒体IP孵化和AI数字文旅服务?", "王海璇的轻旅工作室是单人运营的吗?面向游客和本地文旅商家都服务?", "云迹轻旅有没有做AI相关的文旅技术服务?还做商业咨询和小众旅行定制?"], ["启元人工智能科技是教小孩用AI的吗?李瑞康是负责人?学什么专业的?", "D6启元科技是不是专做8到12岁儿童AI科普的?有自己研发的工具包吗?", "启元人工智能科技在共享空间办AI绘画、AI音乐这些少儿体验沙龙吗?", "李瑞康的公司是不是一人OPC?专注儿童AI工具推广和实操体验?", "启元科技的AI实操工具包是为低龄儿童设计的吗?开展过AI视频制作这类活动?"], ["云南派音人工智能科技、昆明舒护安养老服务、云南大学AI+联合创业服务平台这几家为什么在简介里写的是OPC一人公司,但场地安排却不在OPC分区?", "云南大学AI+联合创业服务平台属于哪个分区?它的详细介绍在哪能查到?", "简介里说的OPC一人公司项目有哪些被分到其他分区了?具体是哪几家?", "云南大学AI+联合创业服务平台是不是在加速10区?它和OPC一人公司的关系是什么?", "‘OPC一人公司’这个说法在简介里出现,但实际场地没分在OPC区,那这些项目到底算不算OPC?比如云南派音、舒护安这些?"], ["一楼直播间总共安排了几家企业?只有1家吗?", "园区一楼的直播间是不是只有一家公司用?是哪家?", "一楼那个直播间是给谁用的?整个一层就一个直播间对吧?", "园区里标着‘E. 一楼直播间’的地方,到底入驻了几家团队?", "一楼直播间这个分区是不是只有一个名额?有没有可能后续加企业?"], ["昆明屿澈电子商务有限公司是做什么的?他们直播卖什么产品?", "昆明屿澈这家公司是不是毕业生创业的?负责人是谁、什么学校毕业的?", "昆明屿澈做的是TikTok Shop跨境直播,主要面向哪些国家?卖什么类型的商品?", "昆明屿澈用AI数字人做24小时直播,还用1688一件代发,这种模式具体是怎么运作的?", "杨君武创办的昆明屿澈,是不是高校毕业生做的东南亚跨境电商项目?用的是AI短视频引流吗?"], ["信息表里‘场地安排’那一栏为空的企业有哪些?一共几家?", "《入驻企业信息表》中没写分区的公司都有谁?比如上古书院是不是其中之一?", "上古书院(云南上古绝学文化发展有限公司)为什么在信息表里没填场地安排?它分到哪个区了?", "园区里有8家企业在信息表里‘场地安排’列是空的,除了上古书院还有哪些?", "‘场地安排’列为空的项目是不是都没分配物理空间?还是只是表格漏填了?上古书院现在实际在哪办公?"], ["入驻企业完整名录一共有多少家?是39家吗?", "39家入驻企业名单在哪能看到?是不是叫‘入驻企业完整名录’?", "赵泳淇负责的是哪家企业?她是不是39家中某一家的负责人?", "赵泳淇是贵州大学毕业的教育技术学专业学生,她创办或负责的企业在39家名录里吗?", "39家企业的负责人信息都公开吗?比如赵泳淇的毕业院校和专业这些细节是从哪来的?"], ["园区里有哪些做国学和家文化传承的公司?", "有没有专门教家训家规或者搞家族文化研学的机构?", "哪家企业是以‘修身齐家’为理念、做国学课程和文化体验的?", "园区里做上古家学文化、能帮定制家训家规的公司叫什么?", "我想找一家把文化体验和家族传承结合起来的国学机构,园区有吗?"], ["中越生物医疗这个项目具体是做什么的?", "园区里有个叫‘中越生物医疗’的项目,它主要干啥?", "中越生物医疗是企业还是合作项目?属于哪个领域?", "听说园区有中越生物医疗项目,它跟生物医药有关吗?", "中越生物医疗项目是哪个单位在运营?涉及哪些技术方向?"], ["园区入驻企业里有越南籍负责人吗?叫什么名字?", "武妙英是谁?她在园区哪个公司任职?", "云南大学越南留学生武妙英,在园区负责哪家企业?", "会四门语言(中英越法)的越南籍负责人在园区哪家企业?", "谁是园区里那个越南籍、在云大读工商管理、懂四国语言的企业负责人?"], ["中越生物医疗项目做的蛋白生产有什么特别之处?", "这个中越联合实验室生产的生物蛋白用在哪些地方?", "中越生物医疗项目已经拿到越南哪些认证了?", "他们说覆盖越南80%以上市场缺口,具体是哪类蛋白产品?", "中越生物医疗的蛋白生产怎么做到合规跨境出口的?满足疫苗和试剂需求吗?"], ["园区有没有做中越之间旅游或文化交流的项目?", "中越文旅交流项目具体开展哪些活动?", "想了解中越两国在文化旅游方面的合作,园区有相关项目吗?", "中越文旅交流是企业还是政府合作项目?主要面向什么人群?", "园区里叫‘中越文旅交流’的项目,和旅游、非遗、语言学习有关吗?"], ["阮胡竹珠是谁?她在园区哪个企业工作?她是云南民族大学哪个专业的学生?她什么时候毕业?", "园区里叫阮胡竹珠的负责人是哪一家企业的?她的学校和专业是什么?", "入驻企业名单里有位叫阮胡竹珠的同学,能查到她的详细信息吗?比如学校、专业、毕业时间?", "云南民族大学广播电视专业的在读生阮胡竹珠,在园区负责哪家企业?", "阮胡竹珠是2027届毕业生?她在园区具体做什么?"], ["园区里有没有做中越旅游交流的公司?主要提供哪些服务?", "哪家企业专门搞中越文化旅游合作?比如旅游定制、语言服务这些?", "做中越旅游宣传和新媒体运营的是哪家入驻企业?有双语能力吗?", "有没有企业既懂越南语又会做旅游短视频和活动策划的?", "搭建中越文旅平台的企业,具体能帮游客做什么?比如订行程、体验文化这些?"], ["‘绿态’环保科技项目是哪家企业?它主要做什么业务?", "园区里叫‘绿态’的项目是干什么的?属于哪个团队?", "‘绿态’这个名称对应的是哪个入驻企业?有什么特色?", "有没有叫‘绿态’的环保类创业项目在园区?", "‘绿态’环保科技项目是独立企业还是某个团队的项目名称?"], ["陈国旗是哪家企业负责人?他是留学生吗?负责什么工作?", "园区入驻企业里有位叫陈国旗的越南留学生,他在做什么业务?", "负责中越供应链和中国分销的那位越南留学生是谁?属于哪家公司?", "陈国旗在园区管海外工厂对接和国内销售渠道,他具体负责哪家企业?", "哪位入驻企业负责人同时管越南工厂和中国全渠道销售?是不是叫陈国旗?"], ["‘绿态’项目做的环保餐具原料是什么?是不是用槟榔叶做的?", "天然槟榔叶餐具是哪家企业研发的?在越南生产、中国销售吗?", "禁塑令背景下做环保餐具的企业有哪些?怎么卖到中国的?有抖音直播吗?", "‘绿态’的餐具在越南哪里生产?怎么运到昆明?在园区有仓库和直播间吗?", "用mo cau(槟榔叶)做的可降解餐具,是哪个园区企业做的?清关和销售模式是怎样的?"], ["昆明滇油石化有限责任公司是哪家企业?", "滇油石化在昆明吗?", "昆明滇油石化有限责任公司全称是什么?", "滇油石化这家公司名字怎么写?"], ["入驻园区的企业一共有多少家?", "园区入驻企业名单总共有39家对吗?", "李政德是谁?他在园区负责什么?", "李政德是云南国土资源职业学院哪个专业的?他什么时候毕业?", "地球物理勘探专业是哪个学校开的?李政德读的是本科还是专科?"], ["生物基纳米驱油剂是干什么用的?", "这家企业主要做低渗透油藏的什么产品?", "他们给中石油、中石化哪些油田提供服务?", "老油田二次采油和三次采油有什么区别?他们方案绿色高效体现在哪?", "一带一路沿线哪些国家的老油田会用他们的产品?"], ["Facebook越南市场卖普洱茶和建水紫陶壶这个项目具体是做什么的?", "有没有在Facebook上卖云南普洱茶和建水紫陶的跨境项目?", "越南市场做普洱茶电商是不是用Facebook平台?", "建水紫陶壶出口越南,是通过Facebook做的吗?", "这个跨境电商项目名称叫什么?"], ["罗有艳是哪个公司的负责人?", "罗有艳学的是越南语专业吗?哪年毕业的?", "云南农业大学越南语专业是本科吗?罗有艳是2020届的吗?", "罗有艳负责的项目跟越南有关吗?", "罗有艳毕业于云南农大哪个学院?"], ["园区里做普洱茶和建水紫陶跨境生意的是哪家公司?", "有没有专门做越南市场茶叶和茶具出口的企业?用的是Facebook直播带货那种模式吗?", "入驻企业里谁在搞‘内容种草+直播转化+9610报关+越南专线直邮’这一套跨境闭环?", "卖云南手工茶具、普洱茶,还主打越南市场的公司有几家?都在哪个园区?", "我想找做中式茶礼套装跨境零售的团队,他们是不是靠Facebook主页、短视频和广告引流?"], ["鬼才明AI创意工作室是干啥的?名字挺特别,是正式注册的项目吗?", "园区里有个叫‘鬼才明’的AI工作室,具体做什么方向?接不接企业定制开发?", "听说有个‘鬼才明AI创意工作室’,是学生创业项目还是老师带队的?", "‘鬼才明AI创意工作室’属于哪个企业或团队?有没有官网或联系方式?", "园区项目列表里的‘鬼才明AI创意工作室’,主要提供哪些AI相关服务?"], ["入驻企业里负责人叫赵广明、学计算机的那家是做什么的?", "赵广明是哪家企业负责人?重庆大学计算机系2022届毕业的,现在在园区做什么业务?", "园区里哪个公司的老板是重大的计算机毕业生,叫赵广明?", "赵广明负责的企业主营什么?他是不是刚毕业就创业了?", "查一下入驻企业名单,负责人姓名是赵广明、专业是计算机、毕业年份2022年的公司信息?"], ["2026年4月启动的那个产教融合AI项目,具体服务政校企哪些方面?", "有没有做AI数字人内容矩阵、还帮学校搞新商科专业规划的公司?", "园区里提供AI职业技能培训、赛事集训和资格认证的机构是哪家?", "哪个团队能帮政府或高校做AI场景应用落地、全域获客和品牌全案?", "聚焦AI商业化落地、数智化品牌赋能,还搞创新创业赛事运营的是哪家入驻企业?"], ["昆明云韵体育公司是不是管男子自行车洲际队的?", "园区里有支男子自行车洲际队,背后运营公司叫啥名字?", "‘昆明云韵体育有限责任公司’除了体育培训,还运营专业自行车队吗?", "想了解园区内职业自行车队的情况,归属哪家公司?是洲际级别吗?", "云韵体育这家公司是干啥的?听说他们带一支男子自行车洲际队?"], ["入驻企业名单里那个舒江贵是谁?他什么时候毕业的?学的什么专业?", "舒江贵是园区哪家企业的负责人?他是不是麒麟区职业学校毕业的?", "运动训练专业毕业的舒江贵,在园区负责哪家企业?", "园区入驻企业负责人舒江贵的学历背景是什么?", "舒江贵是哪年从麒麟区职业学校毕业的?学的是运动训练专业吗?"], ["男子自行车洲际队项目是干什么的?准备什么时候成立?", "云南要建的UCI男子自行车洲际队,具体怎么运营?", "这个自行车队项目是不是要在云南注册?目标是打亚洲比赛吗?", "听说有个高原训练+体育文旅+青苗培养的一体化自行车项目,是哪个?", "男子自行车洲际队项目靠不靠谱?它怎么把职业赛事、商业赞助和文旅结合在一起?"], ["‘男子自行车洲际队项目’在申报材料里被归到哪一类了?", "OPC是什么意思?自行车队项目为什么标着OPC?", "第5节申报名单里,自行车队项目属于OPC类,这个分类对入驻有影响吗?", "OPC类项目包括哪些?自行车队项目为啥被划进OPC?", "园区里还有哪些项目和自行车队一样属于OPC类别?"], ["附件1.3里的31家入驻实体名单完整版能给我看看吗?", "省级申报口径的入驻名单有31家,都有哪些公司或项目?", "‘米勒克尔蓝宝石’‘中越生物’这些项目在31家名单里排第几?", "园区入驻的31家单位里,有没有叫‘男子自行车洲际队项目’的?排多少号?", "‘仰光客厅中缅文化体验营地’‘PineSound派音智能’这些项目都在31家名单里吗?"], ["为什么同一个项目在不同表格里名字不一样?比如‘欧森纳东盟香港留学’和‘中越生物’改名了?", "入驻企业信息表和附件1.3名单名字对不上,是填错了还是有别的原因?", "两份名单项目名称有出入,比如‘米勒克尔蓝宝石’,是因为填报时间不同吗?", "园区不同表格里项目名称不一致,是口径不同还是数据没同步?", "‘中越生物’在两个表里名字不一样,是不是因为改名了?官方以哪个为准?"], ["昆明市大学生创业园的入驻管理规程是哪个部门发布的?", "这份《昆明市大学生创业园创业企业(项目)入驻管理规程》是哪年哪月哪日发布的?", "《昆明市大学生创业园创业企业(项目)入驻管理规程》的发布单位是哪个?", "这个管理规程的出处文件名是什么?", "2026年3月13日发布的园区入驻管理文件全称是什么?"], ["昆明市大学生创业园的主管部门是哪个单位?", "谁在管昆明市大学生创业园的入驻事务?", "负责统筹昆明市大学生创业园入驻工作的政府部门是哪家?", "昆明市人社局是不是创业园的主管单位?", "园区入驻管理归哪个局管?"], ["昆明市大学生创业园的入驻流程由谁具体操作?", "实际帮创业者办入驻手续的是什么机构?", "政府委托哪家单位来执行入驻全流程服务?", "入驻申请找谁办?是人社局直接办还是外包给别的公司?", "第三方服务机构在园区入驻中具体干些什么?"], ["昆明市人社局在入驻管理中只负责监督考核吗?", "主管部门除了监督,还参与入驻审核或审批吗?", "人社局对第三方服务机构怎么管?", "验收入驻工作是谁来做的?", "监督、考核、验收这三项职责都是人社局的吗?"], ["想进昆明市大学生创业园创业,要满足哪些硬性条件?", "申请入驻昆明市大学生创业园需要符合哪些要求?", "园区入驻门槛有哪些?", "大学生创业项目进园区得达到什么标准?", "入园资格审查主要看哪些方面?"], ["我是刚毕业两年的大学生,想创业开公司,能申请入驻园区吗?", "留学生回国创业,没在国内上过大学,可以入驻这个园区吗?", "我毕业已经7年了,还能算作符合条件的青年创业者吗?", "我们公司是外国企业,但想在昆明落地项目,能申请入驻吗?", "如果我不是法人,只是团队负责人,能代表项目申请入驻吗?"], ["园区重点支持哪些行业的创业项目?", "做绿色农产品电商的项目符合园区的产业方向吗?", "搞人工智能应用开发的公司属于园区鼓励的数字经济范畴吗?", "文化旅游和花卉相关的创业项目,园区会优先考虑吗?", "新能源汽车零部件研发算不算新材料或新能源产业链里的项目?"], ["入驻需要提交详细的商业计划书吗?", "启动资金有没有最低要求?没有实际出资能申请吗?", "公司之前有轻微的税务异常记录,还能申请入驻吗?", "团队有成员曾涉及知识产权官司,会影响整个项目入驻吗?", "园区对企业的内部管理制度有具体检查标准吗?比如必须有财务制度或人事规章?"], ["参加过‘春城创业荟’拿了二等奖,入驻时有加分或优先权吗?", "我们团队有国家发明专利,但没得过奖,算优先入驻对象吗?", "博士期间发了多篇顶刊论文,算‘突出学术成就’吗?", "获得省级创新创业大赛铜奖,符合优先入驻条件吗?", "手上有2项实用新型专利,能算重大发明创造吗?"], ["入驻园区一共要走几个步骤?大概多久能办完?", "从提交申请到正式入驻,中间要经过哪些环节?", "线上能全程办理入驻手续吗?还是必须现场跑几次?", "入驻流程里需要找园区哪个部门对接?有专门的联系人吗?", "材料初审通过后,后续还有评审或答辩环节吗?"], ["申请园区入驻需要准备哪些材料?", "入驻园区要交哪些表格和证件?", "第一次申请园区入驻,得带身份证、学历证这些吗?", "创业公司还没注册营业执照,能申请园区入驻吗?", "提交入驻申请时,创业计划书和资质专利证书是必须的吗?"], ["材料不全的话园区会怎么处理?", "初审没通过是不是就直接被拒了?", "服务机构初审时发现材料缺东西,会告诉我补什么吗?", "初审阶段如果漏交了某份文件,还能补交吗?", "园区初审是一次性指出所有问题,还是分几次通知?"], ["入驻评审是怎么进行的?", "路演答辩环节具体怎么安排?", "评审专家是从哪里请来的?是昆明本地的吗?", "入驻打分满分100,多少分才算过关?", "评审结果是当场出还是等几天?80分以上是不是就稳了?"], ["团队资质这块评分占多少分?", "评审时怎么看我们团队行不行?", "学历不高但有多年创业经验,团队分会不会很低?", "团队人员流动性大,会影响20分里的‘人员稳定’项吗?", "专业跟项目不完全对口,‘专业匹配度’这部分还能拿高分吗?"], ["项目创新性这部分占多少分?", "没有专利但有独特商业模式,算不算有创新性?", "技术壁垒指的是什么?我们用的是成熟技术但做了优化,算吗?", "自主知识产权必须是发明专利才行吗?软著或实用新型可以吗?", "行业壁垒高但技术本身不新,创新性这25分还能拿满吗?"], ["企业入驻评分里‘产业适配与市场前景’占多少分?这部分主要看哪些方面?", "匹配重点产业链、市场需求、竞品分析这些,是不是都算在25分的‘产业适配与市场前景’里?", "入驻打分中,市场拓展能力和盈利模式归在哪项?分值是多少?", "跟园区重点产业链贴合度不高,会影响入驻评分吗?这项占几分?", "‘产业适配与市场前景’25分具体怎么打分?需要提供竞品分析报告吗?"], ["运营规划和落地能力占多少分?商业计划书不完整会被扣分吗?", "启动资金和经营管理制度这些,属于评审里的哪一块?分值是多少?", "入驻打分中,有没有阶段目标和落地方案的要求?占多少分?", "光有想法没有详细执行计划,会影响入驻吗?对应的是哪项评分标准?", "‘运营规划与落地能力’15分,具体要提交哪些材料来证明?"], ["社会价值与发展潜力占多少分?带动就业和税收预期各占多大比重?", "入驻评审里,行业潜力和可持续发展归在哪一项?分值是多少?", "如果项目能带动几十人就业,但短期税收不高,社会价值这块能得高分吗?", "‘社会价值与发展潜力’15分,是不是主要看能招多少人、交多少税?", "园区看重长期发展,那行业潜力和可持续发展在评分里怎么体现?分值多少?"], ["入驻评审结果要在哪个网站公示?公示期多久?", "人社局官网公示后没人提意见,是不是就自动算通过了?", "评审完还要报哪个部门备案?备案之后才公示吗?", "公示环节是必须的吗?没公示能不能直接入驻?", "市人社局官网公示无异议后,下一步是什么流程?"], ["签《入驻孵化协议》是在公示之后还是之前?", "协议签完就能马上拿到工位吗?办公场地怎么分配?", "正式入驻的标志是签协议还是拿到工位?", "《入驻孵化协议》签了以后,多久能安排办公场地?", "签完协议、分到工位,是不是就代表可以正式入驻开始办公了?"], ["OPC创业空间适合哪些类型的创业项目?能待多久?", "大学生没注册公司,能在园区哪个区域孵化?最长能待几年?", "创业苗圃区和加速区有什么区别?没注册的团队能进加速区吗?", "国际创客区是给谁用的?配套服务跟其他区一样吗?", "归国留学生创业,能申请哪个孵化区域?入驻时间怎么算?"], ["孵化加速区对企业注册状态有要求吗?最多能待几年?", "初创企业申请加速区,需要提供营业执照吗?最长能续多久?", "已注册的初创公司,适合申请哪个孵化区域?能住满3年吗?", "加速区和国际创客区的入驻时长一样吗?国际创客区也要注册公司吗?", "国际创客区的‘个性化办公空间’具体指什么?跟加速区的独立办公空间一样吗?"], ["园区每年怎么考核入驻企业?考核内容包括哪些方面?", "入驻企业每年要参加什么考核?考核标准是怎么定的?", "园区对创业项目的年度评估都看哪些指标?有没有打分规则?", "企业年度考核会查营收、专利这些吗?还有别的考核点吗?", "园区怎么判断一家企业是不是适合继续留在孵化器里?"], ["经营发展能力在考核里占多少分?具体看哪些指标?", "技术创新能力考核包括研发投入和产学研合作吗?分值是多少?", "合规履约情况具体考什么?比如材料报送不及时会扣分吗?", "创业带动成效是不是要看招了多少应届生?实习岗位也算分吗?", "发展潜力这块主要看融资和资质认证?满分15分怎么打?"], ["年度考核多少分算合格?不合格会怎么样?", "考核80分以上就是优秀?有没有奖励或优先续签?", "如果考核只有59分,会被清退吗?有没有补救机会?", "考核结果分几个等级?每个等级对应什么后续安排?", "园区对考核不合格的企业会直接赶出去,还是先提醒整改?"], ["企业孵化不合格会被赶出去吗?", "孵化不合格会收到什么通知?", "整改后还是不合格,园区会怎么处理?", "整改通知书是什么意思?", "孵化协议会被解除吗?"], ["园区企业退出有哪几种情况?", "企业退出园区分哪些类型?", "退出管理包括哪些情形?", "企业从园区离开有哪些方式?", "园区对退出是怎么分类的?"], ["什么情况下算成功毕业出园?", "企业满足哪些条件就能出园了?", "出孵标准具体有哪些?", "怎么才算孵化成熟可以离开?", "自主核心产品规模化是不是出园条件之一?"], ["孵化期满了不续签会怎样?", "孵化到期没申请延期怎么办?", "延期申请没通过是不是就得走?", "已经延期过一次,到期了还能再延吗?", "孵化合同到期不续,园区会让我搬走吗?"], ["企业自己想退出园区要怎么办?", "因为经营困难能主动申请退出吗?", "团队散了还能申请退出吗?", "主动退园需要走什么流程?", "不想继续孵化了,怎么向园区提出退出?"], ["企业入驻后多久没人办公会被强制清退?", "哪些情况会导致园区勒令企业退出?比如空置、违规、转租这些算吗?", "企业被吊销营业执照或者破产了,园区会怎么处理?", "不配合检查、虚报数据或者擅自改项目,会被赶出去吗?", "连续几天办公室没人,或者一个月内累计多少天没人办公就算违规?"], ["园区平面图里那个带玻璃顶的地方是哪里?", "中庭、共享工位区和休息厅在图上分别是什么颜色、在什么位置?", "整个楼层布局是怎么安排的?比如大厅、过道、房间这些怎么分布的?", "顶部左侧的‘玻璃顶’和中上部的‘大厅’是不是同一个区域?", "最下面一排有9个房间,它们是连着一条过道的吗?"], ["成长区、国际区、加速区各有多少间办公室?", "共享工位区、大小会议室和玻璃顶这些算不算独立分区?数量怎么算?", "‘OPC’在布局图里对应哪个区?和宣传材料里的分区命名一致吗?", "休息厅、过道、玻璃顶这些功能空间有没有编号或具体数量?", "详细的布局图里提到的‘楼道’有多少条?分布在哪些位置?"], ["玻璃顶、成长区、国际区这些分区在楼层里是怎么上下左右排列的?", "共享工位区和休息厅是在楼层中间偏上的位置吗?", "加速区是排在最下面一排还是沿着过道分布的?", "大小会议室和加速区都在过道附近吗?它们和楼道的关系是什么?", "《入驻企业信息表》里的‘加速/国际/成长/OPC’分类,在实际布局里怎么一一对应的?"], ["昆明市创业园宣传册一共多少页?主要内容有哪些?", "宣传册里有没有介绍园区的空间布局或者分区类型?", "16页彩页的宣传册里,图片部分包含哪些关键信息?", "宣传册PDF文件里有没有提到企业入驻流程或管理规定?", "这份宣传册是官方发布的吗?主要面向哪些人群?"], ["园区的定位和核心理念是什么?", "这个园区主要想实现什么目标?", "园区倡导的理念有哪些?", "园区是怎么定义自己的发展方向的?", "园区的整体定位和愿景是什么?"], ["这个园区是免费的吗?", "大学生创业能在这里得到免费孵化服务吗?", "是不是政府办的创业园区?", "有没有专门针对青年大学生的创业孵化基地?", "这是不是云南第一家政府主办的大学生创业孵化园?"], ["园区是哪一年成立的?", "昆明市人社局主办的创业园什么时候开始运营的?", "这个园区在2012年获得了什么称号?", "园区具体地址在哪里?在几楼?", "园区总面积有多大?在昆明哪个位置?"], ["园区提供哪些创业支持服务?", "什么是‘七位一体’创业扶持体系?", "园区能帮创业者解决哪些问题?", "有没有AI相关的创业支持?", "园区能不能提供融资、政策、资源等一站式服务?"], ["园区面向南亚东南亚有什么特别规划吗?", "园区怎么体现国际化定位?", "AI技术在园区服务中怎么应用?", "来昆明创业的年轻人,这里是不是第一站?", "园区是不是专门为来昆明创业就业的青年打造的‘家园’?"], ["园区政策服务包括哪些主要板块?", "政策服务一共有几大类?", "宣传册里提到的政策服务有哪七个部分?", "园区的政策服务具体分哪几块?"], ["园区提供免费办公场地吗?", "新创企业能申请免费工位或独立办公室吗?", "办公桌椅、文件柜、高速网络这些基础设备园区包不包?", "有没有免费的基础办公设施可以使用?", "初创团队能拿到免费办公空间吗?"], ["园区有没有创业指导服务?", "有没有一对一的创业辅导?", "园区会帮创业者梳理商业模式和对接市场吗?", "创业沙龙、项目路演这些活动定期办吗?", "有没有专家团队帮我们做技术指导和创业规划?"], ["园区能帮忙找投资吗?", "有没有种子资金或者合作基金可以申请?", "园区能推荐银行贷款或股权融资渠道吗?", "创业初期怎么获得融资支持?", "园区跟创投机构有合作吗?"], ["园区能帮忙申请创业补贴吗?", "税收减免和社保补贴这些政策园区能协助办理吗?", "高校毕业生创业有哪些扶持政策,园区能帮着落实吗?", "创业扶持资金怎么申请?园区会协助吗?", "政策对接服务具体都包括哪些内容?"], ["园区能帮我们对接高校和科研院所吗?", "如果想跟昆明的大学或大公司合作技术或者招人,园区能帮忙吗?", "园区是不是属于昆明创业创新服务体系?能参加‘春城创业荟’这类活动吗?", "我们项目想做成果转化,园区能提供对接支持吗?", "园区有没有资源帮初创企业找技术合作伙伴或者引进人才?"], ["园区有没有免费的AI培训?都教些什么内容?", "想学怎么用AI做内容创作或者开一人公司,园区有相关培训吗?", "听说园区有AI公益课,能教企业怎么用AI省钱增效吗?", "园区的AI培训是实操性质的吗?有没有老师手把手教?", "除了AI应用,园区还开哪些创业相关的免费培训课?"], ["注册公司的事园区能代办吗?", "刚创业,税务和知识产权这些专业事园区能帮忙处理吗?", "园区提供法律援助服务吗?比如合同纠纷或者劳动问题?", "工商注册、报税这些流程复杂,园区有专人协助办理吗?", "知识产权咨询是免费的吗?包括商标和专利方面的指导?"], ["在校大学生能申请入驻园区吗?", "毕业6年的本科生还能以创业者身份申请入园吗?", "外籍留学生创业,符合园区的入驻条件吗?", "园区重点支持哪些行业的创业项目?比如做生物医药或者新能源的行不行?", "我们项目是做数字文旅的,属于昆明‘10+N’产业链里吗?"], ["入驻需要提交详细的商业计划书吗?", "启动资金要多少才够?园区对创业者的资金能力有硬性要求吗?", "公司之前没出过问题,但没申请过专利,会影响入驻吗?", "如果团队有发明专利或者拿过市级创业比赛奖,是不是更容易入园?", "园区查不查企业的信用记录?比如有没有违法或经营异常?"], ["入驻园区要走哪些步骤?整个流程是怎样的?", "从咨询到正式入驻,一共要经过哪几个环节?", "路演答辩后多少分才算通过?评审结果在哪公示?", "入驻评审要答辩吗?有没有打分标准?多少分能过?", "市人社局官微会公示哪些信息?公示期多久?"], ["OPC创业空间适合做AI应用的团队吗?能直接拎包入住吗?", "我们是还没注册公司的大学生团队,能申请哪个区?共享工位能用多久?", "创业苗圃区待满两年后,表现好还能继续留园吗?怎么转区?", "孵化加速区给初创企业配的是独立办公室吗?最长能待几年?", "留学生回国创业,该申请哪个区域?配套和普通区一样吗?"], ["国际创客区和孵化加速区的入驻时长规定是一样的吗?", "归国留学生申请国际创客区,办公空间是定制的吗?有哪些国际化配套?", "孵化加速区的企业续期需要什么条件?最多能待三年对吗?", "初创企业已经注册了,但刚成立一个月,符合孵化加速区要求吗?", "创业苗圃区和孵化加速区在工位类型上有什么区别?"], ["咨询电话是多少?打哪个号问入驻报名最靠谱?", "18087174891这个号码是打给谁的?报统计表也打这个吗?", "云南省就业局那边怎么申报?和园区直报流程一样吗?", "报名咨询除了打电话,还有别的联系方式吗?比如微信或网站?", "填报人电话和宣传册上的咨询电话重复了,是不是同一个联系人?"], ["园区办公地址在哪?具体到门牌号和楼层?", "昆明市大学生创业园是在昆明市人力资源中心里面吗?", "去现场咨询要上几楼?是在民航路229号的二楼吗?", "人力资源中心二楼除了创业园,还有其他办事窗口吗?需要预约吗?", "导航搜‘昆明市大学生创业园’能准确定位到民航路229号吗?"], ["报名材料发到哪个邮箱?", "报名要发邮件到什么地址?", "接收报名的邮箱是多少?", "报名是通过邮箱提交吗?", "714066050@qq.com 是报名用的邮箱吗?"], ["园区都跟哪些媒体合作做宣传?", "园区怎么对外宣传创业典型?", "电视、广播、报纸这些渠道园区都用吗?", "地铁专列和网络直播也是园区的宣传方式吗?", "园区的媒体报道覆盖哪些平台类型?"], ["园区合作的全国性媒体有哪些?", "央视网、人民网这些算不算园区的合作媒体?", "抖音和今日头条是不是园区的全国合作媒体?", "网易搜狐这些大平台跟园区有宣传合作吗?", "CCTV.com和people.cn是园区对接的主流媒体吗?"], ["云南本地的媒体哪些跟园区有合作?", "云南日报和云南台是不是园区合作的省级媒体?", "春城晚报和云南网也在园区的合作名单里吗?", "Yunnan Media Group指的是云南广播电视台吗?", "园区在云南省内主要跟哪些媒体联动?"], ["昆明市有哪些媒体和园区合作?", "昆明日报和昆明广播电视台是不是园区的市级合作媒体?", "掌上春城和无线昆明是园区合作的本地平台吗?", "KMCC是不是指掌上春城?它跟园区有宣传合作吗?", "园区在昆明市主要通过哪些媒体做报道?"], ["园区的新闻热线是多少?", "想联系园区发新闻稿,电话是多少?", "8099999这个号码是园区的媒体热线吗?", "打哪个电话可以对接园区的媒体合作?", "园区对外的新闻联络方式有哪些?"], ["园区宣传册上用的数据口径是哪些?", "对外宣传时园区推荐用哪些官方数据?", "新闻报道里提到的园区数据,有没有统一的标准说法?", "园区有没有指定的对外宣传数据模板或参考口径?", "做宣传材料时,哪些园区数据是官方认可、可以引用的?"], ["昆明市政府网站上怎么介绍园区的规模和成效?", "园区运营多久了?有多少企业入驻?带动多少创业者?", "昆明市政府官网说园区入驻企业超1000家,这数据是截至什么时候的?", "园区运营超过10年,带动创业者超1万人次,这个说法出自哪里?", "昆明市政府网站提到的园区数据(比如1000+企业、1万+创业者)具体是哪篇报道的口径?"], ["春城晚报报道过园区孵化企业的成绩吗?", "园区近3年孵化了多少家企业?培育了多少高新和科小企业?", "春城晚报说园区孵化超100家企业,其中5家是国家级高新,12家是省级科小,这准确吗?", "有没有媒体报道过园区在培育高新技术企业方面的成果?", "春城晚报提到的园区企业培育数据(如5家国高、12家省科小)具体指什么?"], ["昆明日报最近报道过园区新增企业情况吗?", "2024年以来园区新入驻了多少家企业?其中高新企业有多少?", "园区今年办了多少场活动?覆盖多少青年创业者?", "昆明日报说园区2024年新增32家企业、举办126场活动,这些数据能确认吗?", "昆明日报提到的‘2024年以来新增高新技术企业15家’,是指新认定还是新入驻?"], ["媒体报道里说的孵化企业数量跟政府报表上的数字不一样,这是怎么回事?", "对外宣传时提到的孵化企业数和官方统计的累计数据有出入,该怎么正确引用?", "为什么媒体报道口径和省级申报、月报表里的累计数据不一致?", "园区对外发布的孵化企业数、出园企业数、带动就业人数这些数据,是不是和官方统计有差别?", "媒体报道中用的那些数据,比如孵化181家,跟人社局正式报表里的数字对不上,哪个更权威?"], ["昆明市大学生创业园是哪年成立的?", "园区什么时候被评为省级创业园的?", "2024年4月园区焕新亮相是怎么回事?都有哪些媒体报了?", "央视网5G、中国网这些大媒体啥时候报道了园区焕新?", "园区2024年4月15日发布的第二期入驻招募公告,跟第一期评审结果公示是一起发的吗?"], ["2024年5月园区办了哪些重要活动?", "春城晚报和云南日报啥时候整版报道了园区?标题叫啥?", "‘聚力赋能·共创未来’这个高校创业活动是谁主办的?有哪些学校参与?", "昆明城市学院师生啥时候来园区参观交流的?", "青年企业家协会啥时候走进园区揭牌创新创业基地的?"], ["《入驻管理规程》是哪年哪月印发的?谁发的?", "2026年第一期入驻申请截止后,初审通过了多少个?复审又收了多少个?", "第一期集中评审具体怎么评的?流程是啥?", "2026年第一期最终有多少家企业或项目正式入驻了?", "2026年第二期入驻招募公告是啥时候发布的?"], ["‘创赢未来’2026创业大赛昆明选拔赛是啥时候结束的?谁主办的?", "马兰花创业培训讲师大赛跟园区有关系吗?是不是在园区办的?", "2026年创业大赛昆明选拔赛和讲师大赛是不是同一个活动?", "昆明市人社局主办的这场创业大赛,跟园区日常运营有啥关联?", "‘创赢未来’创业大赛昆明选拔赛落幕那天,园区有没有参与或者协办?"], ["昆明市公共就业和人才服务中心主办的创业路演活动什么时候开始?一共几天?有多少项目进了省赛?", "‘滇水逐梦·雨林创航’2026昆明·西双版纳创业路演PK赛是哪几家单位联合办的?园区里哪家企业参与了活动策划?", "园区最近有没有对入驻企业做实地摸排?摸排了多少家?发现什么问题?怎么处理的?", "云南师范大学和园区搞的就业创业实践基地揭牌仪式,具体有哪些国家的留学生参加了?主题是不是叫‘校地产教融合、跨境双创人才共育’?", "云南旅游职业学院和云南师范大学这两所学校,最近分别什么时候来园区开展就业创业交流活动的?"], ["园区的官方名称全称是什么?在哪些正式文件或设计稿里有明确出处?", "昆明市大学生创业园的定稿文件除了.ai源文件,还提供了什么格式的版本?比如PNG或者JPG?", "园区的官方文件定稿是叫《昆明市大学生创业园 定稿》吗?这个标题出现在哪里?", "园区有没有统一的品牌视觉识别规范?比如Logo、字体、配色这些?", "‘昆明市大学生创业园’这个名称在品牌VI里是怎么写的?英文全称是什么?"], ["园区Logo是什么样子的?颜色搭配是哪两种?字母K和M是怎么组合的?", "园区的主Logo是橙色和蓝色搭配吗?那个圆环形/动感造型是不是由K和M组成的?", "园区Logo里的英文全称怎么拼?是不是‘KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK’?", "园区VI里最常用的排版方式是怎样的?比如Logo和文字一般怎么摆放?", "园区品牌标识强调橙蓝配色,这个‘橙’和‘蓝’具体指哪种橙、哪种蓝?有没有标准色值?"], ["园区品牌VI规定的主色有哪些?橙色和蓝色的具体色值是多少?", "VI规范里说的品牌主色,是不是用设计软件拾色器取的数值?能直接用在PS或AI里吗?", "园区橙色和蓝色的标准色号分别是多少?CMYK、RGB或者HEX值有公布吗?", "除了橙蓝双色,园区VI里还有没有其他辅助色或禁用色规定?", "品牌主色的取值依据是设计稿里的拾色器读数,那这个‘设计稿’指的是哪个版本的文件?"], ["园区Logo的橙蓝双色组合,和VI里定义的品牌主色是不是完全一致?有没有细微差别?", "‘橙色K’和‘深蓝M’交织的Logo造型,在不同尺寸(比如名片vs展板)下会不会有简化版?", "园区对外宣传材料中,Logo必须居中排版吗?有没有允许左对齐或其他变体?", "英文全称‘KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK’在VI里有没有大小写或缩写规范?", "VI规范第14条讲的是品牌设计,那前面13条都包括哪些内容?比如字体、图标、应用延展这些?"], ["园区的主色调橙色和辅助色深蓝色的十六进制代码分别是多少?", "活力橙和深邃蓝对应的RGB数值各是多少?", "品牌VI里用的主色和辅色具体叫什么名字?分别用在哪些地方?", "橙色#FC8318和蓝色#022B97哪个是主色哪个是辅色?它们各自代表什么含义?", "园区LOGO和正文文字分别用的是哪种颜色?主色橙和辅色蓝的具体用途有哪些?"], ["昆明市大学生创业园的品牌VI源文件叫什么名字?在哪里能拿到?", "‘昆明市大学生创业园 定稿.ai’这个AI文件是做什么用的?", "主画板成品图和色彩规范参考图的文件名分别是什么?格式是png还是jpg?", "VI设计规范里提到的三个配套文件具体指哪几个?各自用途是什么?", "想改LOGO或者做宣传物料,该找哪个源文件?有没有成品图和配色参考图?"], ["绩效数据里的‘认定以来累计’‘本年度’‘当前实有’分别是什么意思?", "为什么本月报表说累计出园149户,省级申报却写150户?差这1户是怎么回事?", "不同表格中数据口径不一致时,应该以哪个为准?", "‘填报时点不同’具体指什么?会影响数据准确性吗?", "数据口径说明里提到的‘照原文保留’是什么意思?是不是不能自行修改差异?"], ["绩效表里‘累计数’和‘当年数’怎么区分?分别对应哪些章节?", "第2节绩效和第3节月报表的数据分类方式一样吗?", "‘认定以来累计’‘本年度’‘当前实有’这三个统计维度在报表里怎么体现?", "园区绩效数据的统计逻辑是按自然年、财政年还是认定周期?", "为什么同一指标(比如出园户数)在不同表格里数值不一样?背后的数据逻辑是什么?"], ["园区现在一共入驻了多少个项目?内部统计和省级申报为什么数量不一样?", "信息表列了39家,省级申报只列31家,少了8家是没通过认定吗?", "内部全量项目名单和省级认定名单的项目名称为啥有的不一样?是改名了吗?", "39家和31家这两个数字分别代表什么范围?是否包含孵化中和已出园的项目?", "项目名称在不同名单里有出入,是命名规范不统一,还是认定标准导致的差异?"], ["宣传册里写的媒体报道数据和官方申报的累计数据不一样,该怎么引用才规范?", "外宣口径和官方申报口径的数据对不上,引用的时候要注意什么?", "宣传册里的媒体报道数据跟别的地方不一样,引用时要怎么注明来源?", "第12节提示里说的引用出处具体指什么?"], ["文件里的数值是直接抄的原始资料,还是自己算出来的?", "这些数字是原始数据还是加工过的?", "文档里的数值有没有经过推算或调整?", "所有数据都是直接从原始文件拿的,没改过吧?"], ["这份文档是哪年生成的?", "文档生成日期写的是2026年,是未来时间吗?是不是填错了?", "整理这份文档用的原始文件格式有哪些?", "文档说是根据Excel、PDF这些原始资料整理的,具体支持哪些格式?"]], "embed_texts": ["> 口径说明:以下政策与数据基于公开资料整理,截至 2026 年 8 月。涉及具体补贴金额、名额、申报条件时,以云南省及昆明市最新官方文件为准;本库作为语义检索与回答口径的参考。", "这个知识库里的政策和数据是最新版的吗?", "补贴金额和申报条件以哪里为准?", "你们说的截至2026年8月是什么意思?", "如果官方文件更新了,这里的内容还准不准?", "这个知识库主要是用来干啥的?", "一、什么是 OPC(一人公司)\n- 定义:一人公司(One Person Company, OPC)指只有一个股东的公司,分为一人有限责任公司和一人股份有限公司两类。", "一个人能开公司吗?", "什么叫一人公司?OPC是啥意思?", "一人有限责任公司和一人股份有限公司有啥区别?", "只有一个股东的公司合法吗?", "OPC是不是就是只有一个老板的公司?", "一、什么是 OPC(一人公司)\n- 法律沿革:传统公司法理论认为公司属社团法人、应有 2 人以上股东;我国自 2005 年修订《公司法》起正式引入一人有限责任公司。", "一人公司是啥时候开始在中国合法的?", "2005年《公司法》修订跟一人公司有啥关系?", "以前为啥不允许一个人开公司?", "一人有限责任公司最早是哪年写进法律的?", "中国是从什么时候起承认一人公司的?", "一、什么是 OPC(一人公司)\n- 时代定位:在 AI 时代,一人公司成为低成本、高效率、普惠型的创业形态——创业者仅凭一人之力,借助 AI 工具即可构建覆盖产品研发、市场营销、客户服务与财务管理的完整业务闭环。", "AI时代为啥适合搞一人公司?", "一个人用AI能干成哪些事?", "现在一个人创业靠AI真能搞定研发、营销、客服和财务吗?", "为啥说一人公司是低成本高效率的创业方式?", "AI时代的一人公司跟传统小作坊有啥不一样?", "一、什么是 OPC(一人公司)\n- 核心理念:「你负责商业判断,AI 负责高效执行」,实现从\"出卖单次时间\"到\"经营自动化系统\"的跨越。", "‘你负责商业判断,AI负责高效执行’这句话啥意思?", "一人公司怎么从卖时间变成经营系统?", "怎么理解‘经营自动化系统’这个说法?", "一人公司里人和AI具体怎么分工?", "这句话说的‘跨越’到底指什么变化?", "一、什么是 OPC(一人公司)\n- 与自由职业的区别:一人公司具备单一股东、有限责任、极简架构等正规企业属性,同时兼具灵活高效、税负更可控等优势。", "一人公司和自由职业者有啥区别?", "OPC算不算正规公司?跟个体户、 freelancer比有啥优势?", "一人公司是不是有限责任?架构是不是特别简单?", "为啥说一人公司税负更可控?它和普通公司比灵活在哪?", "一、什么是 OPC(一人公司)\n- 规模:截至 2025 年 6 月,全国一人有限责任公司已超过 1600 万家。", "现在全国有多少家一人公司?", "截至2025年6月,一人有限责任公司总数大概多少?", "一人公司数量破千万了吗?最新统计数据是多少?", "全国一人公司注册量现在到什么规模了?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 总特征:准入放宽与责任强化并存。", "新《公司法》对一人公司是放宽还是收紧了?", "2024年7月1日实施的新公司法,对一人公司主要有哪些变化?", "新公司法下一人公司是更容易注册了还是责任更大了?", "新《公司法》对一人公司的总原则是什么?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 允许设立一人股份有限公司:设立股份有限公司应有一人以上二百人以下发起人,一名股东也可设立股份有限公司。", "一个人能开股份有限公司吗?", "新公司法允许一个自然人设立股份有限公司吗?", "一人股份公司合法吗?需要几个股东才能成立股份公司?", "以前说股份公司至少两人,现在改了吗?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 取消一人股东数量限制:删除旧法\"一个自然人只能投资设立一个一人有限责任公司,且该一人公司不能投资设立新的一人公司\"的规定;一个自然人可设多个一人公司,可形成投资链条。", "一个人还能注册多家一人公司吗?", "新公司法取消了‘一个自然人只能开一家一人公司’的规定吗?", "我之前注册了一人公司,还能再注册第二家吗?", "一人公司之间还能互相投资成立新公司吗?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 放宽股东类型:删除\"一人公司股东只能是自然人或者法人\"的限制,个体工商户、合伙企业、个人独资企业等理论上也可作股东设立一人公司。", "新公司法实施后,个体工商户能自己开一人公司吗?", "合伙企业可以当一人公司的股东吗?", "个人独资企业现在能注册成一人公司的老板不?", "2024年7月1号起,除了自然人和法人,还有哪些主体能当一人公司股东?", "新《公司法》是不是把一人公司股东范围扩大到个体户、合伙企业这些了?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除专节规定:不再采用\"一人有限责任公司\"或\"一人股份有限公司\"的特殊表述,统一为\"只有一个股东的公司\",纳入普通公司规制。", "新公司法还分‘一人有限责任公司’这种专门类型吗?", "现在注册只有一个股东的公司,营业执照上还写‘一人有限公司’吗?", "新法里是不是取消了‘一人股份有限公司’的说法?", "只有一个股东的公司,现在法律上怎么称呼?还叫‘一人公司’吗?", "新《公司法》是不是把一人公司直接当成普通公司来管,不再单列一节了?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除年度审计强制要求:不再强制一人公司每年编制财务会计报告并审计。", "新公司法下,一人公司每年还要强制做审计吗?", "只有一个股东的公司,2024年7月后还必须请会计师事务所审账吗?", "现在一人公司还能自己编财务报告,不用找人审计不?", "新《公司法》取消了对一人公司的年度审计硬性要求,是真的吗?", "一人公司今年开始是不是不用每年都搞财务审计了?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除登记特别注明:不再要求在登记及营业执照中注明\"自然人独资\"或\"法人独资\"。", "新公司法实施后,营业执照上还会标‘自然人独资’或‘法人独资’吗?", "注册一人公司时,登记信息里还要特别注明是独资性质吗?", "现在办营业执照,系统还让选‘自然人独资’这个选项吗?", "一人公司的工商登记和执照上,是不是不再体现‘独资’字样了?", "新法取消了独资标注要求,那别人怎么知道这家公司是一个人开的?", "(责任强化,需重点注意)", "新《公司法》对一人公司最大的责任变化是什么?", "一人公司股东现在要承担什么更强的责任?", "听说新公司法加强了一人公司的股东责任,具体指哪块?", "为什么说‘责任强化’是重点注意事项?会带来什么实际影响?", "一人公司老板在新法下要特别注意哪些法律责任风险?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 举证责任倒置:新《公司法》第 23 条第 3 款规定,只有一个股东的公司,股东不能证明公司财产独立于股东自己财产的,应对公司债务承担连带责任。", "新公司法实施后,一人公司的股东如果没法证明公司财产和自己个人财产分开,是不是要对公司债务一起负责?", "2024年7月1日新公司法生效后,一人公司股东举证责任倒置具体指什么?", "一人有限公司的老板要是说不清公司钱和自己钱的区别,会不会被要求连带还债?", "新《公司法》第23条第三款关于一人公司股东连带责任的规定,到底怎么理解?", "一人公司股东怎么避免因财产不独立而承担连带责任?新法里举证责任倒置是啥意思?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 财产混同是主要认定依据:司法实践中,公司账户与股东个人账户资金往来频繁是财产混同的主要认定依据。", "法院一般怎么判断一人公司存在财产混同?账户之间转钱多算不算?", "公司账上和个人银行卡老有资金往来,会被认定为财产混同吗?", "一人公司股东和公司之间经常互相转账,是不是很容易被认定为财产混同?", "司法实践中,认定一人公司财产混同最主要看什么?是不是就看银行流水?", "公司跟股东之间频繁打款,是不是财产混同最直接的证据?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- AI 时代\"数字资产混同\"风险:用个人名义注册付费订阅 AI 大模型、购买云端算力、将训练数据与生成物存于个人账号,一旦发生纠纷,可能被认定公司缺乏独立财产基础,判令股东承担连带责任。", "用我自己的微信/支付宝订阅ChatGPT Plus,算不算公司财产不独立?", "AI时代下,用个人账号买大模型服务、存训练数据,会不会让一人公司被认定为没独立财产?", "我把公司用的AI算力、生成内容都存在个人云盘里,新公司法下有风险吗?", "以个人名义开通的云端GPU服务,给公司项目用,会被认为财产混同吗?", "在个人账号里管理公司AI资产(比如API密钥、训练数据、生成结果),新法实施后会否导致股东担责?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 不设股东会时须书面决定:股东就利润分配、亏损弥补、修改章程等事项,须以书面形式决定,签名或盖章后置备于公司。", "一人公司没有股东会,利润怎么分?必须写成书面文件吗?", "新公司法要求一人公司股东做决定必须签字留底,哪些事必须书面决定?", "一人公司修改章程或分利润,不开会的话,光口头说行不行?", "股东自己决定分红或补亏,是不是得签个字或者盖个章,然后放在公司备查?", "新《公司法》规定一人公司重大事项须书面决定,具体包括哪些?怎么操作才合规?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 建立健全财务管理制度,账目清晰、独立核算,严格区分公司与个人财产界限。", "一人公司要怎么管账才算符合新公司法要求?", "新公司法强调财务独立,一人公司是不是必须单独建账、单独报税?", "公司账和老板私人账混着记,新法下有什么风险?", "一人公司如何做到账目清晰、独立核算?有没有实操建议?", "建立健全财务管理制度对一人公司来说,具体要落实到哪些动作上?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 适时出具连续完整的会计年度审计报告,或股东与公司往来专项审计报告,作为财产独立性的有力证据。", "新公司法实施后,一人公司怎么证明财产独立?需要做哪些审计?", "2024年7月1日新公司法生效后,一人有限公司必须每年做审计报告吗?", "一人公司股东和公司之间资金往来,要怎么审计才能避免连带责任?", "新《公司法》要求一人公司提供连续完整的会计年度审计报告,具体指什么?", "除了年度审计,一人公司还能用什么审计报告来证明财产独立?", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 将合规视野从\"财务隔离\"拓展到\"数据与数字资产隔离\"。", "新公司法说的‘数据与数字资产隔离’是什么意思?跟以前的财务隔离有啥不同?", "一人公司现在不仅要管好钱,还要管好数字资产?具体要怎么做?", "新《公司法》对一人公司的合规要求是不是从钱扩展到数据了?", "什么叫‘数字资产隔离’?一人公司怎么确保客户数据、AI模型这些不和股东个人混同?", "新公司法提到的数据隔离,是指公司微信账号、域名、数据库这些也要和股东分开吗?", "三、OPC 创业思路与经验\n- 先完成,再完美:AI 已把门槛拆掉,不要等学会所有技能再开始。", "AI时代创业还用等学完所有技能再开始吗?", "听说现在创业讲究‘先完成再完美’,是不是真的不用准备充分就能启动?", "AI把创业门槛降低了,那新手到底该什么时候开始做第一个项目?", "‘先完成,再完美’在OPC创业里具体怎么操作?举个例子?", "不会编程、不会设计,能直接用AI开公司吗?需要先学多久?", "三、OPC 创业思路与经验\n- 最小成本验证模式:用 AI 思考与决策,用外包解决人力,用最小成本验证商业假设。", "最小成本验证模式具体怎么操作?比如第一步该做什么?", "用AI做决策+外包干活,这种最小验证方式适合哪些类型的创业项目?", "没多少启动资金,怎么用最小成本验证我的商业想法靠不靠谱?", "OPC创业中说的‘最小成本验证’,和MVP(最小可行产品)是一回事吗?", "AI思考+外包人力,这个组合在验证阶段容易踩哪些坑?", "三、OPC 创业思路与经验\n- 聚焦四力:产品能力(解决什么问题)、流量能力(客户如何找到你)、交付能力(如何高效完成服务)、变现能力(如何设计商业模式)。", "OPC创业说的‘四力’是哪四力?分别指什么?", "产品能力、流量能力、交付能力、变现能力,这四个能力怎么判断自己有没有?", "创业初期资源有限,应该优先提升四力中的哪一力?", "为什么OPC特别强调这四个能力?少了其中一力会有什么风险?", "怎么评估自己的流量能力和变现能力是否匹配?有没有简单自测方法?", "(盈利三把钥匙与商业模式)", "盈利三把钥匙具体指哪三个?", "商业模式里说的‘盈利三把钥匙’是什么意思?", "怎么理解用AI降本增效、用信任创造溢价、用平台借势腾飞这三句话?", "OPC提到的盈利三把钥匙,有没有更接地气的解释?", "盈利三把钥匙和普通创业方法有啥不一样?", "三、OPC 创业思路与经验\n- 盈利三把钥匙:用 AI 降本增效、用信任创造溢价、用平台借势腾飞。", "OPC推荐的主流商业模式有哪些?", "做个人创业,有哪些靠谱的变现方式?", "知识付费、内容变现、服务外包这些模式具体都包括啥?", "SaaS工具、付费社群、混合模式分别适合什么人做?", "有没有把六大商业模式讲清楚的例子或对比?", "三、OPC 创业思路与经验\n- 六大主流商业模式:知识付费(课程/咨询/私教)、内容变现(公众号/小红书/抖音)、服务外包(代运营/代写/代设计)、工具产品(模板/插件/SaaS)、社群运营(付费社群/会员制)、混合模式。", "OPC创业落地的完整步骤是怎样的?", "从零开始做个人创业,该按什么顺序一步步来?", "怎么用最低成本验证一个想法靠不靠谱?", "什么是MVP测试?私域池和自动化引流矩阵怎么搭?", "怎么设计能抗风险的收入结构?标准化交付系统怎么做?", "三、OPC 创业思路与经验\n- 从盘点个人技能 → 验证市场需求 → 找准差异化定位 → 用极低成本测试 MVP → 搭建自动化内容引流矩阵与私域池 → 打造标准化交付质控系统 → 设计高溢价产品矩阵 → 构建抗风险的多元化收入线。", "电商创业案例里那个河南创业者是怎么做到月入几万的?", "卧床70天还能做跨境电商?他具体用了哪些AI工具?", "用扣子搭工作流生成经营日报,这个怎么操作?", "12家网店为啥最后只留4家?优化逻辑是啥?", "每天只干2-3小时,单店月净利2.5万以上,普通人能复制吗?", "三、OPC 创业思路与经验\n- 电商:河南创业者卧床 70 天,用 AI 替代原计划的 10 人团队做跨境电商,开出 12 家网店(后优化至 4 家主力店),单店月销 5-8 万、月净利 2.5-3 万,每天只需工作 2-3 小时;用扣子搭工作流自动生成经营日报。", "盈利三把钥匙和六大商业模式之间有啥关系?", "创业步骤里提到的‘高溢价产品矩阵’怎么跟‘用信任创造溢价’挂钩?", "AI降本增效在河南电商案例里是怎么体现的?", "自动化引流矩阵和内容变现、社群运营这些模式怎么配合?", "整个OPC创业思路里,最核心、最先该动手的是哪一步?", "三、OPC 创业思路与经验\n- 内容与服务:无锡创业者 2025 年转型\"企业陪跑\",帮企业做小红书内容转化;指出 OPC 两大难题是\"如何定价\"与\"如何建立交易机制(个人即产品、前期缺乏标准化交付物)\"。", "OPC创业做小红书内容转化服务,具体怎么帮企业提升转化?2025年无锡创业者转型‘企业陪跑’,定价和交易机制这两块到底该怎么解决?", "无锡那边有创业者在2025年搞OPC小红书陪跑服务,听说卡在定价和交付标准化上——这俩难题实际中怎么破?", "OPC模式里说‘个人即产品’、前期没标准交付物,那小红书内容转化服务怎么跟客户谈价格、签合同?", "做企业小红书陪跑的OPC项目,除了内容制作,还包含哪些服务?定价逻辑和交易流程一般怎么设计?", "听说OPC创业在小红书转化这块,最大痛点是定价难+没标准化交付——有没有落地的解决方案或参考案例?", "三、OPC 创业思路与经验\n- 全 AI 员工公司:创业者用开源 AI 代理框架创建\"零人类公司\",业务含卖教程、AI 技能市场抽佣、企业定制代理,几周收入近 20 万美元,运营成本仅约每月 1500 美元。", "那个全AI员工的公司是怎么运作的?用什么开源AI代理框架搭的?卖教程、抽佣、定制代理这三块业务具体怎么赚钱?", "零人类公司几周赚近20万美元,每月成本才1500美元?它靠哪些AI工具实现全自动运营?教程和技能市场怎么搭建的?", "用开源AI代理做的‘全AI员工公司’,真实案例里AI到底负责哪些环节?企业定制代理和抽佣平台怎么落地?", "不雇人、纯AI跑的公司,收入来源是不是就教程销售、AI技能市场佣金、企业代理定制这三项?每块大概占比多少?", "全AI运营公司月成本只要1500美元,它用的是LangChain还是AutoGen这类框架?技术栈和商业模型能拆解下吗?", "三、OPC 创业思路与经验\n- 大厂裸辞:35 岁管理者裸辞环游多国,靠 AI 创立一人公司,兼任大模型工具开发者、企业咨询顾问、自媒体博主,年入近百万。", "35岁大厂管理者裸辞环游世界后做一人公司,具体靠哪些AI相关业务年入近百万?都干了啥角色?", "裸辞后靠AI搞一人公司,同时当大模型工具开发者、企业顾问和自媒体博主——这三重身份怎么协同变现?", "大厂裸辞转AI一人公司,年入百万的真实路径是啥?开发工具、接咨询、做自媒体,哪块收入占比最高?", "35岁裸辞环游多国还能靠AI年入百万,他做的大模型工具有哪些?企业咨询主要解决什么问题?", "一人公司模式下,怎么平衡大模型开发、企业顾问和自媒体三件事?时间分配和收入结构大概是怎样的?", "(清醒认知与挑战)", "OPC创业里提到的‘清醒认知与挑战’具体指哪些方面?新手最容易踩哪些坑?", "关于OPC创业的清醒认知部分,有没有总结过核心挑战点?比如心态、资源、能力短板这些?", "‘清醒认知与挑战’这块没展开说,实际做OPC时最常被忽略的认知盲区是什么?", "OPC创业者需要具备哪些关键清醒认知?比如对自身能力、市场周期、失败概率的预判?", "除了赚钱门槛没消失,OPC路上还有哪些隐性挑战?比如客户信任建立、长期可持续性、精力管理?", "三、OPC 创业思路与经验\n- AI 没有消灭赚钱的门槛:认知、决策能力甚至运气同样重要;没有商业逻辑,再好的工具也没用。", "为什么说AI没消灭赚钱门槛?光有工具但没商业逻辑,到底会出什么问题?能举个实际例子吗?", "AI再强也得靠认知和决策能力——这话怎么理解?有没有因为商业逻辑错导致AI项目失败的案例?", "没有商业思维,只堆AI工具会怎样?比如选错目标客户、定价失衡、交付失控这些具体表现?", "AI创业里,‘运气’到底起多大作用?除了认知和决策,还有哪些不可控但关键的因素?", "为什么说决策能力和运气和AI工具一样重要?在OPC实战中,哪些关键决策点最容易翻车?", "三、OPC 创业思路与经验\n- 理性看待收入神话:网上\"一人公司月入十万\"的故事大多不靠谱,成功相当程度依赖运气。", "网上说一人公司月入十万很轻松,真的靠谱吗?", "那些'一人公司轻松月入十万'的说法是不是夸大其词?", "做OPC创业真能随便月入十万?靠谱吗?", "听说有人靠一人公司月入十万,这现实吗?", "OPC创业收入是不是被网上吹得太神了?", "三、OPC 创业思路与经验\n- 一人公司的孤独感与亲力亲为:现金流、回款、定价等都需要自己承担。", "一个人开OPC公司,所有事情都要自己干吗?比如收款、定价这些?", "OPC创业是不是啥都得自己扛,连回款和现金流都要亲力亲为?", "当超级个体户,财务、定价、催款这些活儿都得自己来?", "一人公司是不是从接单到收钱、定价格全得自己操心?", "做OPC会不会特别孤独,而且所有运营事务都得自己搞定?", "(省级专项:2026 年 7 月云南省人社厅《关于支持服务人工智能 OPC 创业的若干措施》)", "云南省有没有专门支持AI方向OPC创业的新政策?是哪年发布的?", "2026年云南人社厅出了个AI OPC创业支持文件,具体叫什么名字?", "云南省关于人工智能OPC创业的省级专项政策是哪个部门发的?", "听说云南今年出了个AI OPC扶持政策,是人社厅出的吗?几月份的?", "云南省最新的人工智能OPC创业支持政策,全称和发布时间是什么?", "四、云南当前生效的 OPC 超级个体政策\n- 创业生态社区:鼓励盘活闲置楼宇、高校实训场地等,整合算力、绿电、数据、市场渠道等要素,打造\"低成本空间+普惠算力+全链条生态服务\"的 AI OPC 创业生态社区;运营效果好的可按创业孵化载体补助政策认定补助。", "云南现在有没有专门为AI OPC创业者建的生态社区?里面提供啥?", "云南的AI OPC创业生态社区能提供算力、绿电这些资源吗?", "云南打造的AI OPC创业社区是不是包含免费空间、普惠算力和全链条服务?", "云南的AI OPC生态社区怎么申请?有补贴吗?", "云南鼓励用高校场地建AI OPC社区,里面整合了哪些要素?能拿补助吗?", "四、云南当前生效的 OPC 超级个体政策\n- 免费工位与安居支持:支持现有创业园区开设 OPC 专区,提供拎包入驻免费工位;有条件地区为 OPC 创业人才提供免租或低租人才公寓、青年驿站。", "云南OPC创业者能申请免费工位吗?在哪儿申请?", "OPC创业者在云南园区能拎包入住免费工位?需要什么条件?", "云南有没有给OPC人才提供的免租公寓或青年驿站?", "在云南做超级个体户,能住人才公寓或者青年驿站吗?收费吗?", "云南园区OPC专区的免费工位和安居支持(比如低租公寓)具体怎么落实?", "四、云南当前生效的 OPC 超级个体政策\n- 金融支持:\"云岭创业贷\"——企业法人最高可申请 500 万元信用贷款、3000 万元抵押担保贷款;个人创业者最高可申请 50 万元信用贷款、1000 万元抵押担保贷款;鼓励推出\"算力贷\"\"数智创新专项信贷\"等专属产品。", "云南现在对超级个体户有啥贷款政策?云岭创业贷能贷多少?算力贷和数智创新专项信贷是啥?", "我是个人创业者,想在云南申请信用贷款或抵押贷款,最高能贷多少?有没有专门支持AI创业的贷款产品?", "云南OPC超级个体政策里的金融支持包括哪些?企业法人和个人创业者分别能贷多少?", "云岭创业贷对企业法人和个人创业者额度一样吗?3000万抵押贷是针对企业的还是个人的?", "听说云南有‘算力贷’和‘数智创新专项信贷’,这些贷款怎么申请?需要啥条件?", "四、云南当前生效的 OPC 超级个体政策\n- 创业补贴:符合条件者给予 5000 元一次性创业补贴;电商创业项目每个最高补贴 3 万元。", "云南给创业者的补贴有多少钱?一次性创业补贴是5000块吗?电商创业能拿多少?", "开网店算电商创业吗?在云南做电商能领最高3万元补贴?要满足啥条件?", "云南OPC政策里的一次性创业补贴和电商项目补贴能同时拿吗?", "刚注册个体户能申5000块创业补贴吗?电商补贴是按项目还是按人发?", "云南对超级个体户的创业补贴具体咋发?是直接打钱还是报销?电商补贴要提供哪些材料?", "四、云南当前生效的 OPC 超级个体政策\n- 见习补贴:吸纳高校毕业生就业见习的按每人每月 1500 元(留用率达 50% 以上可增至 2000 元)发放见习补贴。", "云南企业招高校毕业生见习,每月补贴多少钱?留用率超一半真能涨到2000块?", "我们公司接收应届生见习,每人每月能领多少见习补贴?怎么才算留用率达50%以上?", "云南OPC政策里的见习补贴是给企业还是给学生?1500元和2000元的区别到底在哪?", "高校毕业生见习期一般多长?补贴是按月发还是一次性结清?", "吸纳见习人员后,企业怎么申请这个见习补贴?需要签合同吗?", "四、云南当前生效的 OPC 超级个体政策\n- 培训补贴:AI 相关职业(如数据标注员)纳入培训补贴范围,最高可获 1700 元创业培训补贴和 7280 元技能培训补贴。", "学AI相关技能比如数据标注,在云南培训能拿补贴吗?最多补多少?", "数据标注员培训补贴是7280元?创业培训又是1700元?这两个能一起申吗?", "云南对AI职业的培训补贴覆盖哪些工种?除了数据标注还有哪些算AI相关?", "参加政府认可的AI培训班,补贴是直接发给个人还是给培训机构?", "创业培训补贴和技能培训补贴申请条件一样吗?需要先就业还是可以零基础报名?", "四、云南当前生效的 OPC 超级个体政策\n- 人才政策:符合条件的 AI OPC 创始人纳入\"兴滇英才支持计划\"等省级人才计划;设立人工智能专业职称评审;打破学历、年龄、资历限制建立人才自主评价机制。", "云南AI领域的超级个体创始人能评职称吗?人工智能专业职称怎么评?", "没学历、年纪大、没资历,能在云南评AI方向的人才称号吗?兴滇英才计划怎么申报?", "OPC创始人进‘兴滇英才支持计划’有啥好处?人才自主评价机制具体怎么操作?", "人工智能专业职称评审打破学历限制是真的吗?需要发表论文或专利吗?", "云南对AI创业者的省级人才认定有哪些?除了兴滇英才还有别的计划吗?", "四、云南当前生效的 OPC 超级个体政策\n- 政务服务:深化\"个人创业一件事\"集成办理,实现 OPC 企业登记一表申请、一网联办、一次办结。", "云南现在办OPC超级个体营业执照,能一次搞定所有手续吗?", "在云南注册OPC超级个体,是不是填一张表、上一个网站就能办完?", "云南的‘个人创业一件事’集成服务具体怎么操作?", "OPC超级个体在云南能不能一网通办、一次办结?", "云南对OPC超级个体有没有政务服务优化措施?", "(市级:昆明市大学生创业园)", "昆明市大学生创业园属于市级还是省级的孵化平台?", "昆明市大学生创业园是哪个层级的创业支持载体?", "想申请昆明市大学生创业园入驻,这个园区归谁管?", "昆明市大学生创业园是不是市级政府主办的?", "云南的OPC政策里提到的市级创业园具体指哪个?", "四、云南当前生效的 OPC 超级个体政策\n- 面向对象:在校大学生、毕业不超过 8 年的高校毕业生、归国及外籍留学生等青年创业者。", "云南OPC超级个体政策面向哪些年轻人?", "在校大学生和毕业几年内的毕业生能申请云南OPC吗?", "留学生包括归国和外籍的,能不能享受云南OPC政策?", "毕业超过5年但没到8年的高校毕业生,还符合云南OPC申请条件吗?", "云南OPC政策里说的‘青年创业者’具体包括哪些人群?", "四、云南当前生效的 OPC 超级个体政策\n- 入驻方向:项目须聚焦昆明市\"10+N\"产业链(人工智能、数字经济、电子信息、新媒体直播、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等);获市级以上创业大赛奖项、有突出学术成就或高新专利者优先。", "云南OPC超级个体项目必须做哪些行业才算符合要求?", "昆明市‘10+N’产业链都包括哪些方向?比如人工智能算不算?", "做新媒体直播或生物医药的项目,能不能申请云南OPC?", "拿过市级创业比赛奖的项目,在云南OPC入驻时有优先权吗?", "有发明专利或者学术成果,申请云南OPC会不会更容易通过?", "四、云南当前生效的 OPC 超级个体政策\n- 空间保障:免费提供办公工位或独立区域。", "云南OPC超级个体能免费用办公场地吗?", "入驻云南OPC有没有工位或办公室提供?", "OPC超级个体在云南能申请免费独立办公区吗?", "云南给OPC创业者提供的是共享工位还是单间?", "云南OPC政策里的空间保障具体是免费提供什么?", "四、云南当前生效的 OPC 超级个体政策\n- 孵化服务:专家一对一创业指导、商业模式梳理、沙龙路演。", "云南OPC超级个体政策里,孵化服务具体包括哪些内容?", "创业初期能获得哪些一对一指导?", "有没有商业模式梳理和路演机会?", "云南的OPC孵化服务包含专家辅导吗?", "想参加创业沙龙或项目路演,云南OPC政策支持吗?", "四、云南当前生效的 OPC 超级个体政策\n- 融资支持:联动创投机构设立种子资金,每年组织不少于 15 场融资对接活动。", "云南OPC政策有融资支持吗?具体怎么帮创业者找钱?", "种子资金是政府出的还是创投机构出的?", "每年组织多少场融资对接活动?", "OPC创业者能参加融资路演或对接会吗?", "云南有没有专门针对超级个体的早期投资渠道?", "四、云南当前生效的 OPC 超级个体政策\n- 政策对接:协助申请创业扶持资金、税收减免、社保补贴。", "云南OPC政策能帮忙申请哪些创业扶持资金?", "税收减免和社保补贴具体怎么申请?", "政策对接服务包括哪些实际帮扶?", "个人创业者在云南能享受哪些财政补贴?", "OPC个体户可以申请减税或社保补助吗?", "四、云南当前生效的 OPC 超级个体政策\n- 资源整合:对接高校、科研院所、龙头企业。", "云南OPC政策提到资源整合,都对接哪些单位?", "高校、科研院所和大企业怎么跟OPC创业者对接?", "作为个体创业者,能通过OPC链接到龙头企业资源吗?", "云南OPC能帮我联系大学或研究院合作吗?", "政策里的‘资源整合’是指拉通产学研资源吗?", "四、云南当前生效的 OPC 超级个体政策\n- 专项赋能:免费提供\"AI 育笋计划\"和\"AI+OPC\"全套培训(涵盖 AI 应用、一人公司落地、成本控制等)。", "云南OPC的AI育笋计划是免费的吗?都教什么?", "‘AI+OPC’培训包含一人公司注册和成本控制吗?", "有没有面向超级个体的AI应用实操课?", "AI育笋计划和AI+OPC培训是一回事吗?", "免费AI培训覆盖从入门到落地的全流程吗?", "四、云南当前生效的 OPC 超级个体政策\n- 综合服务:工商注册、税务代办、知识产权咨询、法律援助一站式支持。", "云南现在OPC超级个体政策有哪些服务?", "在云南做OPC超级个体,能享受哪些代办和咨询服务?", "云南OPC政策里说的一站式支持都包括啥?工商税务知识产权法律这些都能帮办吗?", "云南OPC超级个体能免费办营业执照和报税吗?", "想在云南搞OPC,工商注册、税务、法务、知产这些事有人全程代办吗?", "(OPC 核心范式)", "OPC的核心范式是什么?", "OPC最根本的运作方式或底层逻辑是啥?", "什么叫OPC核心范式?能用大白话解释一下吗?", "OPC和其他创业模式最大的区别在哪?是不是就靠那个核心范式?", "OPC强调的‘核心范式’具体指哪一套方法论或结构?", "五、OPC 的几种创业模式\n- OPC 的核心是「1 个创始人 + N 个 AI 数字员工」的 1+N 模式:创始人负责战略决策、创意设计与关键价值判断,AI 承担代码生成、内容创作、数据运营、客户服务等标准化重复工作。", "OPC创业是不是只要1个人加几个AI就能干?创始人和AI各自干啥?", "OPC的1+N模式里,人负责什么、AI又干哪些活?", "听说OPC是‘1个创始人+N个AI员工’,这N个AI到底做哪些重复性工作?", "OPC中创始人是不是只管拍板和创意,其他都甩给AI?", "OPC的1+N模式具体怎么分工?比如写代码、做客服、搞运营这些谁来干?", "五、OPC 的几种创业模式\n- OPC 将创业从「资本密集型」转向「认知密集型」:无需融资和组建团队,只需对领域有深度认知,再用 AI 将其转化为产品、服务或收入。", "OPC创业为啥说从资本密集变成认知密集了?", "不融资、不招人,光靠脑子就能创业?OPC真这么轻松?", "OPC强调认知密集型创业,是不是意味着懂行比有钱更重要?", "OPC不需要团队和投资,那靠啥赚钱?是不是全看创始人对行业的理解深度?", "为什么说OPC把创业重心从砸钱变成了拼认知?有没有实际例子?", "五、OPC 的几种创业模式\n- 创业门槛从「资源门槛」降为「认知门槛」:代码可用 Claude Code 写、内容可用 AI 生成、跨语言可实时翻译、客户对接可用 AI Agent 筛选。", "OPC创业门槛真的变低了吗?现在开公司还需要懂编程会外语吗?", "OPC说门槛从资源降到认知,具体哪些事AI能替我搞定?", "不会写代码、英语不好、没客户资源,还能做OPC吗?AI能帮我补上这些短板吗?", "用Claude写代码、AI生成内容、实时翻译、AI筛选客户——这些在OPC里都是标配吗?", "OPC里提到的AI工具链(比如Claude Code、AI Agent)普通人能直接上手用吗?需要技术基础吗?", "(六大已跑通的 OPC 商业模式)", "OPC目前有哪些已经落地验证的商业模式?", "OPC跑通了哪六种商业路径?", "哪些OPC商业模式已经被实际验证可行?", "园区说的六大已跑通的OPC模式具体指哪些?", "OPC领域里哪些商业模式已经有成功案例了?", "五、OPC 的几种创业模式\n- 专业服务型:以深度专业技能为核心价值,交付定制化解决方案或咨询,按项目或小时收费,利润率高。", "专业服务型OPC具体是做什么的?", "靠专业技能接项目、按小时或项目收费的OPC模式叫什么?", "深度定制化咨询、高利润率的OPC创业方式是什么?", "以专业能力为核心、做定制方案的OPC类型怎么称呼?", "OPC里那种靠专家经验卖服务、不卖产品的模式叫啥?", "五、OPC 的几种创业模式\n- 高端服务型:以行业认知与战略判断力为核心,提供高端顾问、私教、战略规划,高客单(一单可达几十万)。", "高端服务型OPC适合什么人做?", "一单能收几十万的战略顾问、私教类OPC属于哪种模式?", "靠行业洞察和战略判断力赚钱的OPC类型叫什么?", "OPC中主打高端顾问、私董会、战略规划的是哪一类?", "强调认知高度和决策能力、客单价特别高的OPC模式是哪种?", "五、OPC 的几种创业模式\n- 知识产品型:将系统化专业知识做成标准化数字产品(课程、电子书、会员),边际交付成本趋近于零,最被看好。", "知识产品型OPC主要卖什么?", "把专业知识做成课程、电子书、会员的OPC模式叫什么?", "边际成本几乎为零、靠标准化内容变现的OPC类型是哪种?", "系统化知识打包成数字产品的OPC创业方式怎么称呼?", "为什么说知识产品型OPC最被看好?它具体怎么做?", "五、OPC 的几种创业模式\n- 数字产品型:可复用的软件或数字工具(SaaS、应用、插件、模板),订阅制或一次性/API 收费,规模化潜力最大。", "数字产品型OPC包括哪些具体形式?", "SaaS、模板、插件这类可复用工具属于哪种OPC模式?", "靠订阅制或API收费的软件类OPC创业方式叫什么?", "能规模化复制、有最大增长潜力的OPC类型是哪一种?", "OPC里做应用、工具、自动化模板的是哪种模式?", "五、OPC 的几种创业模式\n- 创意内容型:靠个人影响力或 IP,产出博客、视频、社媒内容,通过广告分成、品牌合作、知识付费变现。", "OPC创业里,靠写博客、拍视频、做社交媒体赚钱属于哪种模式?", "如果我主要靠个人IP接广告、做知识付费,这算OPC里的什么创业类型?", "创意内容型OPC具体怎么变现?有哪些常见方式?", "做自媒体、短视频、小红书博主,属于OPC五种模式里的哪一种?", "靠影响力产出内容并赚钱,是OPC中的‘创意内容型’吗?", "五、OPC 的几种创业模式\n- 平台整合型:做资源连接与撮合,自身不生产产品,靠佣金或撮合费。", "OPC里不自己生产产品、只帮别人牵线搭桥收佣金的模式叫什么?", "做中间人撮合资源、收服务费或佣金,属于OPC的哪种创业方式?", "平台整合型OPC是不是就是当‘二道贩子’或者‘中介’?", "没有自有产品,靠连接供需双方赚钱,这是OPC里的平台整合型吗?", "OPC中强调资源对接和撮合收费的模式具体指什么?", "(如何选择适合你的模式)", "怎么判断OPC哪种创业模式最适合我?", "新手想入局OPC,该怎么选适合自己的创业路径?", "有没有简单的方法帮我决定该走内容型、顾问型还是平台型?", "选择OPC创业模式时需要考虑哪些关键因素?", "如何结合自身情况匹配最合适的OPC模式?", "五、OPC 的几种创业模式\n- 先想清三个问题:卖「能力」还是卖「系统」?服务边界清不清楚?你是「执行者」还是「系统设计师」?", "OPC创业前必须搞清楚的三个核心问题是什么?", "卖能力还是卖系统?服务边界清不清楚?执行者还是系统设计师?这三个问题到底在问什么?", "OPC选模式时,为什么要想清楚自己是执行者还是系统设计师?", "‘服务边界清不清楚’在OPC创业中具体指什么?怎么判断?", "OPC里说的‘卖能力’和‘卖系统’有什么区别?举个例子?", "五、OPC 的几种创业模式\n- 顾问/咨询型适合专业壁垒深厚、客户关系强的人;AI 可解放线索收集、标准问答、方案初稿。", "OPC中顾问或咨询型适合什么样的人?", "专业强、人脉广的人更适合OPC里的哪种创业模式?", "AI能帮OPC顾问型创业者做哪些事?比如找客户、写方案?", "做咨询类OPC,AI现在能替代哪些重复性工作?", "如果我有多年行业经验但不想天天干活,OPC顾问型+AI辅助靠谱吗?", "五、OPC 的几种创业模式\n- 内容/知识型放大个人 IP,内容可标准化复用、多平台分发,边际成本趋零,适合有表达与知识结构化能力的人。", "我想靠写文章、做短视频创业,有什么适合个人IP的轻量级创业方式?", "有没有那种内容能反复用、发多个平台还不怎么花钱的创业方法?", "我擅长总结知识和表达,有什么创业模式特别适合我这种人?", "做知识类自媒体,怎么把内容变成能长期赚钱的生意?", "个人IP创业是不是就是做公众号、小红书、抖音这些?还有没有别的玩法?", "五、OPC 的几种创业模式\n- 产品化服务型(SaaS/数字产品)将方法论打包成模板或工具,从「卖服务」转向「卖产品」,长期复利最可观但技术含量最高。", "怎么把我的咨询经验变成一个能自动卖的产品?", "有没有办法把服务做成像软件那样一次开发、多次销售?", "想从接单做项目转成卖标准化数字产品,该怎么做?", "SaaS或者数字工具这类产品化服务创业,难在哪里?", "把专业方法论打包成模板或工具卖,具体要怎么操作?", "五、OPC 的几种创业模式\n- 热门赛道:健康/医疗/心理支持、硬件/IoT/机器人、教育/培训/知识服务、金融/法律/政务等专业服务、企业服务/SaaS。", "现在哪些行业特别适合一个人创业做OPC?", "健康、教育、企业服务这些热门赛道,普通人入局机会大吗?", "做心理支持或IoT硬件方向的一人公司,需要什么基础?", "金融、法律这类专业服务,一个人能干得起来吗?", "机器人、SaaS、知识付费……哪些赛道更适合新手试水OPC?", "六、如何成立一家 OPC(一人公司)\n- 先副业后主体:不建议立即辞职,先以副业验证模式。", "刚想搞一人公司,是应该先辞职全职干,还是边上班边试试?", "副业验证模式具体指什么?要做多久才算验证成功?", "不辞职直接注册公司行不行?风险大不大?", "为什么说别一上来就辞职做OPC?有没有真实踩坑案例?", "副业阶段该重点测试哪些东西,才能判断值不值得转正职?", "六、如何成立一家 OPC(一人公司)\n- 个体工商户注册快、税负低,适合早期验证、接小单;有限责任公司信用强、利于 B 端,适合长期发展,但务必公私分明避免财产混同。", "个体户和有限公司开一人公司,到底选哪个好?", "刚起步接小活,注册个体户是不是最省事省钱?", "想以后跟企业合作,是不是必须注册有限责任公司?", "一人有限公司怎么避免老板个人财产和公司财产混在一起?", "个体户税少但信用弱,有限公司信用好但麻烦多,怎么平衡?", "六、如何成立一家 OPC(一人公司)\n- 若业务面向 B 端长期合作选有限公司,早期验证可选个体户。", "成立一人公司,是注册有限公司好还是个体户好?", "B端长期合作该选有限公司还是个体户?", "刚创业做产品验证,适合注册个体户还是有限公司?", "一人公司选有限公司和个体户各有什么优缺点?", "面向企业客户长期合作,注册一人有限公司合适吗?", "六、如何成立一家 OPC(一人公司)\n- 公司名称核准:提交 3-5 个备选名称(行政区划+字号+行业+有限公司),经市场监管局官网或「一网通办/一窗通」平台核准。", "公司起名要准备几个备选名字?", "注册一人公司时,公司名称怎么取才容易通过?", "在‘一网通办’上核名要填哪些信息?", "公司名称格式是不是必须包含行政区划、字号、行业和‘有限公司’?", "市场监管局官网核名需要提交多少个名称备选?", "六、如何成立一家 OPC(一人公司)\n- 提交设立登记申请:线上提交公司章程、股东身份证明、注册地址证明等,约 3-5 个工作日领取营业执照。", "注册一人公司线上要提交哪些材料?", "设立登记申请要准备公司章程、身份证和地址证明吗?", "网上提交一人公司注册申请后多久能拿到营业执照?", "注册一人有限公司,线上办理大概要几天?", "营业执照一般多久能下来?", "六、如何成立一家 OPC(一人公司)\n- 刻制印章:公章、财务章、法人章,到公安备案刻章点办理,费用约 500 元。", "一人公司刻章要刻哪几枚章?", "公章、财务章、法人章都要刻吗?去哪刻?", "刻公司印章要多少钱?大概费用是多少?", "公安备案的刻章点在哪里找?需要预约吗?", "刻章必须去公安指定地点吗?费用大概500块对吗?", "六、如何成立一家 OPC(一人公司)\n- 银行开户:预约开基本户,需带执照、公章、法人身份证,年费约 500-1000 元。", "一人公司开基本户要带什么材料?", "银行开户要预约吗?需要法人本人去吗?", "开公司对公账户年费一般是多少?", "营业执照、公章和法人身份证都带齐了就能当场开户吗?", "基本户年费500到1000元,是所有银行都这样吗?", "六、如何成立一家 OPC(一人公司)\n- 税务登记:领取执照后 30 日内办理,核定税种、申请发票。", "领取营业执照后多久要办税务登记?", "公司注册完要多久去税务局报到?", "办完执照后30天内必须做税务登记吗?", "税务登记要核定哪些税种?", "新公司怎么申请发票?", "六、如何成立一家 OPC(一人公司)\n- 新《公司法》允许单人担任法人且无需监事。", "一个人能当公司法人吗?", "一人公司还需要设监事吗?", "现在一个人开公司,自己当老板还要请别人当监事吗?", "新公司法允许只有一名股东兼法人吗?", "一人有限公司的法定代表人可以是唯一股东吗?", "(注册资本与地址)", "注册公司对注册资本有啥要求?", "注册地址有什么规定?", "公司注册时地址必须是商用吗?", "注册资本和注册地址分别要注意啥?", "注册公司时地址和资金方面有哪些硬性条件?", "六、如何成立一家 OPC(一人公司)\n- 实行认缴制,注册时无需实缴全部资金;但须在章程明确认缴期限,且 5 年内实缴到位。", "一人公司注册时要马上把钱打到公司账户吗?", "认缴制下注册资金是不是可以先不交?", "公司章程里要写清楚多久交齐注册资本吗?", "认缴期限最长能写几年?", "5年内必须把认缴的注册资本全部实缴到位吗?", "六、如何成立一家 OPC(一人公司)\n- 认缴≠不缴:切忌盲目追高注册资本,经营不善时股东需在认缴范围内承担责任。", "认缴的注册资本没交完会有什么风险?", "注册资本写太高会有什么后果?", "公司亏了,股东要拿自己钱补注册资本吗?", "‘认缴’是不是等于不用缴?", "如果公司经营不善,股东要按认缴金额赔钱吗?", "六、如何成立一家 OPC(一人公司)\n- 注册地址须为商用地址(写字楼、商铺、共享办公工位、集中登记地/虚拟地址等),住宅原则上不可用。", "注册一人公司时,住宅地址能当注册地址吗?", "一人有限责任公司可以用家里地址注册吗?", "注册OPC必须用商用地址吗?住宅行不行?", "写字楼、商铺、共享办公这些算商用地址吗?", "虚拟地址和集中登记地属于商用地址吗?", "六、如何成立一家 OPC(一人公司)\n- 可租用园区或共享工位,或用集中登记地/虚拟地址(部分园区免费或年费约 3000-6000 元)。", "园区提供的共享工位能用来注册公司吗?", "租用共享办公空间可以作为公司注册地址吗?", "集中登记地或虚拟地址要收费吗?大概多少钱?", "有没有免费的虚拟地址?园区会提供吗?", "3000到6000元一年的虚拟地址服务靠谱吗?", "(记账报税与合规)", "公司没业务也要做账报税吗?", "零收入的一人公司需要记账和报税吗?", "不经营的OPC还要请会计或者报税吗?", "记账报税具体包括哪些事情?", "合规要求里提到的‘合规’主要指哪些方面?", "六、如何成立一家 OPC(一人公司)\n- 公司必须建立账簿,即使零收入也须按月或按季申报(零申报),逾期可能被列入经营异常名录。", "一人公司不赚钱也要做账吗?", "零申报是什么意思?怎么操作?", "不做账或漏报税会有什么后果?", "按月报还是按季报?怎么选?", "被列入经营异常名录会影响个人信用吗?", "六、如何成立一家 OPC(一人公司)\n- 小规模纳税人季度开票≤30 万免增值税(普票);小微企业年应纳税所得额≤300 万按 5% 缴企业所得税(2027 年底前)。", "小规模纳税人开普票,季度30万以内真的不用交增值税吗?", "小微企业企业所得税5%的优惠能持续到什么时候?", "年利润300万以下的企业所得税怎么算?", "2027年底前的税收优惠包括哪些?", "开专票也享受30万免征增值税吗?", "六、如何成立一家 OPC(一人公司)\n- 一人公司利润分配后转账需缴 20% 分红个税。", "一人公司分红要交多少税?", "OPC公司分给老板的钱要扣多少个税?", "一人有限责任公司利润分配后要缴20%个税吗?", "股东从一人公司拿分红,个税怎么算?", "一人公司税后利润转到个人账户要交20%税吗?", "六、如何成立一家 OPC(一人公司)\n- 代理记账费用约 200-500 元/月,起步阶段建议找代账公司。", "代账公司一个月收多少钱?", "刚成立的一人公司请代账大概要花多少?", "初创期找代理记账贵不贵?一般多少钱一个月?", "OPC公司起步阶段建议自己做账还是找代账?费用多少?", "代理记账200到500元/月,是按年付还是一月一结?", "六、如何成立一家 OPC(一人公司)\n- 每年 6 月 30 日前登录「国家企业信用信息公示系统」提交年报。", "一人公司年报什么时候交?", "OPC公司每年必须报年报吗?截止日期是几号?", "国家企业信用信息公示系统年报在哪填?几月几号前完成?", "6月30日前没提交年报会怎样?", "一人有限责任公司年报提交入口和时间要求是什么?", "六、如何成立一家 OPC(一人公司)\n- 严格区分公司财产与股东个人财产,避免公私账户混同,否则股东可能对公司债务承担连带责任。", "一人公司能不能用老板的私人银行卡收公司款?", "公私账户混用会有啥风险?", "为什么说公司钱和老板钱要分开?不分开会担责吗?", "股东个人账户和公司账户混在一起,会被追债吗?", "如何避免一人公司股东对公司债务承担连带责任?", "七、如何选用合适的 AI 工具\n- 按「场景适配」选工具,不要盲目追新:没有哪一款模型在所有任务上全面领先。", "选AI工具是看名气大不大还是看合不合适?", "不同任务该挑哪个AI工具?有没有通用最强的模型?", "为什么不能光挑最新最火的AI用?", "AI工具怎么选才不踩坑?是不是得先想清楚用在啥场景?", "有没有一款AI能搞定所有工作?选工具到底该看啥?", "七、如何选用合适的 AI 工具\n- OPC 中 AI 首先承担软件开发角色(多数团队用 AI 编程),其次才是内容创作。", "AI在园区里主要用来干啥?", "园区里大家最常用AI来做什么?", "AI在咱们这儿是写代码多还是做内容多?", "为啥说AI在园区首先是编程助手?", "AI在软件开发和内容创作之间,园区更侧重哪个?", "七、如何选用合适的 AI 工具\n- 优先选生态开放、能接 API/插件、支持工作流编排的平台,方便搭建自动化。", "选AI工具时最看重什么?", "什么样的AI平台更适合做自动化流程?", "怎么判断一个AI平台能不能接API或者装插件?", "工作流编排是什么意思?哪些AI支持这个功能?", "生态开放的AI工具有哪些特点?", "七、如何选用合适的 AI 工具\n- 写代码、推理、性价比选 DeepSeek(开源、API 定价低)或智谱 GLM(编程强、可私有化)。", "写代码用哪个AI工具好?", "DeepSeek和智谱GLM哪个更适合程序员?", "有没有开源又便宜的编程AI推荐?", "能自己部署的编程AI有哪些?", "DeepSeek和GLM在性价比和私有化方面各有什么优势?", "七、如何选用合适的 AI 工具\n- 中文通用与云生态选通义千问(阿里云百炼 MaaS、长上下文)。", "中文场景下用什么AI最合适?", "通义千问适合哪些具体用途?", "阿里云百炼和通义千问是什么关系?", "长上下文对日常使用有啥帮助?", "通义千问在云服务集成方面强不强?", "七、如何选用合适的 AI 工具\n- 多模态、免费 C 端选豆包(字节),创意内容可用其视频/图像生成。", "豆包能免费用吗?适合做什么?", "字节的豆包能生成视频和图片吗?", "豆包和通义、DeepSeek比起来有啥不同?", "创意类工作比如做海报、剪视频,用哪个AI好?", "C端免费的多模态AI工具推荐哪个?", "七、如何选用合适的 AI 工具\n- 长文档、研报处理选 Kimi(超长上下文)。", "Kimi适合处理哪些类型的文档?", "长篇报告或研报用什么AI工具处理比较好?", "有没有能处理超长文本的AI工具推荐?", "处理几十页PDF文档该用哪个AI?", "Kimi和其它大模型比,长文本处理有什么优势?", "七、如何选用合适的 AI 工具\n- 政企、资质、央国企场景选文心一言(百度,合规资质全)或讯飞星火(语音、信创)。", "政企单位用什么AI工具比较合规?", "央国企内部推荐用哪个国产AI?", "需要资质认证的场景该选文心一言还是讯飞星火?", "百度和科大讯飞的AI工具在政务场景下怎么选?", "做政府项目时,语音识别和信创适配哪个AI更合适?", "七、如何选用合适的 AI 工具\n- 微信生态、快慢思考选腾讯混元。", "微信里能直接用的AI工具有哪些?", "混元在微信生态里怎么用?", "腾讯混元适合做快速决策还是深度分析?", "有没有支持微信公众号/小程序集成的AI工具?", "混元的‘快慢思考’具体指什么功能?", "(Agent 与工作流搭建平台)", "有没有好上手的AI工作流搭建平台?", "非技术人员怎么自己搭AI流程?", "什么是Agent平台?园区推荐哪个?", "AI自动化流程该用什么工具来设计?", "工作流和智能体(Agent)平台有什么区别?", "七、如何选用合适的 AI 工具\n- 扣子(Coze):字节推出的零代码 Agent 平台,拖拽式搭建,适合个人/非技术用户,支持豆包、DeepSeek、Kimi、通义千问、GLM 等模型。", "Coze扣子是哪家公司的产品?", "零代码搭建AI机器人用什么平台?", "扣子支持接入哪些大模型?比如豆包、Kimi这些能用吗?", "个人用户想做个自动问答机器人,用扣子方便吗?", "拖拽式AI Agent平台除了扣子还有别的推荐吗?", "七、如何选用合适的 AI 工具\n- Dify:企业级、技术向,工作流节点丰富,支持国内外主流模型与私有化部署。", "Dify适合什么类型的企业用?", "Dify能支持私有化部署吗?", "Dify支持哪些大模型?", "Dify是偏技术还是偏业务的AI工具?", "Dify的工作流节点多不多?", "七、如何选用合适的 AI 工具\n- 用无代码平台可把客服、内容、竞品、线索、数据等搭建为智能体,月成本可低至约 170 元。", "用无代码平台搭智能体要花多少钱?", "做个客服智能体一个月大概多少钱?", "无代码做内容/竞品/线索智能体贵不贵?", "有没有便宜的AI智能体搭建方式?", "170元能搞定哪些AI智能体?", "八、AI Agent(智能体)概念与一人公司应用\n- AI 智能体是一套能自主理解目标、制定计划、执行操作、自我修正的完整闭环系统,不同于普通聊天机器人。", "AI智能体和普通聊天机器人有啥区别?", "AI智能体是不是就是高级点的Chatbot?", "什么叫‘闭环系统’的AI智能体?", "AI智能体能自己定目标和改方案吗?", "为什么说AI智能体比对话机器人更智能?", "八、AI Agent(智能体)概念与一人公司应用\n- 核心公式:AI Agent = 大模型 + 记忆 + 主动规划 + 工具使用。", "AI智能体由哪几部分组成?", "大模型+记忆+规划+工具调用,这四个缺一不可吗?", "没有记忆或不会调用工具还算AI智能体吗?", "AI智能体的核心公式是什么?", "‘主动规划’在AI智能体里指什么?", "八、AI Agent(智能体)概念与一人公司应用\n- 三大特征:自主决策(自己判断下一步)、工具调用(操作浏览器、调用 API、读写文件、发邮件)、持续学习(优化策略)。", "AI智能体的三个关键能力是啥?", "智能体怎么实现自主决策?", "AI智能体能自动发邮件或查网页吗?", "它能一边干活一边优化自己的方法吗?", "工具调用具体包括哪些操作?", "八、AI Agent(智能体)概念与一人公司应用\n- 成熟智能体通常含五个核心模块:大语言模型(大脑)、任务规划、记忆存储、工具调用、环境感知。", "智能体一般由哪几个核心部分组成?", "一个成熟的AI智能体包含哪些关键模块?", "AI智能体的五大组成部分是什么?", "智能体的‘大脑’指的是什么?它还有哪些配套能力?", "为什么说大模型只是智能体的大脑,还需要其他模块配合?", "(如何搭建一个智能体)", "怎么从零开始搭一个自己的AI智能体?", "普通人能自己动手做一个AI智能体吗?需要哪些步骤?", "搭建智能体的具体操作流程是怎样的?", "有没有适合新手的智能体搭建指南?", "搭建一个可用的AI智能体难不难?要学编程吗?", "八、AI Agent(智能体)概念与一人公司应用\n- 角色设定:赋予具体人员设置与知识边界,上传历史文档。", "给智能体设定角色时要注意什么?", "怎么让AI智能体像某个具体岗位的人一样工作?", "上传历史文档算不算角色设定的一部分?", "角色设定和知识边界分别指什么?怎么设置?", "能不能让智能体只懂我公司的业务,不回答外部问题?", "八、AI Agent(智能体)概念与一人公司应用\n- 知识库挂载(RAG):整理笔记、业务 SOP、白皮书,通过文档切片与向量检索让 AI 具备「记忆」。", "什么是RAG?怎么用它给智能体加‘记忆’?", "怎么让AI记住我的笔记、SOP和白皮书?", "文档切片和向量检索在智能体里起什么作用?", "挂载知识库后,智能体是不是就能回答公司内部问题了?", "RAG是不是就是把我的资料喂给AI让它记住?", "八、AI Agent(智能体)概念与一人公司应用\n- 工作流设计:将重复劳动逻辑化,如自媒体运营分身:聚焦热点、生成文案、生成图表、多平台分发。", "智能体的工作流设计具体怎么做?", "自媒体运营类智能体是怎么自动完成热点追踪→写文案→做图→分发的?", "怎么把日常重复工作变成智能体可执行的流程?", "工作流设计和普通自动化脚本有啥区别?", "一人公司怎么用工作流设计让一个智能体顶好几个岗位?", "八、AI Agent(智能体)概念与一人公司应用\n- 插件与 API 集成:接入搜索、数据库、API 插件,让智能体具备「手脚」。", "AI智能体怎么才能像真人一样有手有脚去干活?", "智能体怎么接入外部工具比如搜索和数据库?", "让AI智能体具备实际操作能力,需要接哪些插件或API?", "AI智能体的‘手脚’具体指什么?怎么实现?", "智能体怎么调用外部服务来执行任务?", "八、AI Agent(智能体)概念与一人公司应用\n- 避坑:防止幻觉(强制优先从知识库取答案)、遵循 MVP 原则不过度设计、定期分析对话日志迭代提示词。", "怎么防止AI胡说八道乱编答案?", "做AI智能体时最容易踩哪些坑?", "MVP原则在AI智能体开发中怎么落地?", "怎么通过对话日志优化提示词?", "智能体上线后该定期看哪些数据来迭代?", "(OPC 的 1+N 组织架构)", "OPC的1+N组织架构到底是什么意思?", "OPC模式里‘1’和‘N’分别代表什么?", "什么是1+N架构?跟传统公司结构有啥不一样?", "OPC组织里核心层和数字员工是怎么分工的?", "1+N架构中的N是固定人数还是可伸缩的?", "八、AI Agent(智能体)概念与一人公司应用\n- 核心层(1 人)只做四件事:定战略、定标准、审关键输出、管现金流。", "一人公司老板每天到底该干哪四件事?", "核心层那一个人只做战略、标准、审核和管钱吗?", "定标准和审关键输出具体指哪些工作?", "现金流管理在一人公司里为什么必须由核心层亲自抓?", "一人公司的创始人是不是就只管这四件事,其他全交给AI?", "八、AI Agent(智能体)概念与一人公司应用\n- AI 智能体层(N 个数字员工):按岗位拆分为独立 Agent,如产品/研发、内容/营销、流量/投放、销售/客服、交付/项目、财务/行政。", "AI数字员工有哪些常见岗位类型?", "一个AI智能体能当销售客服,还能当财务行政吗?", "产品岗和营销岗的AI智能体是不是要分开建?", "流量投放和内容创作能用同一个AI智能体搞定吗?", "交付项目和财务行政这类不同职能,为啥要拆成独立Agent?", "八、AI Agent(智能体)概念与一人公司应用\n- 外部协作层:把设计、剪辑、翻译等 AI 搞不定的重活外包。", "AI搞不定的设计、剪辑、翻译这些活儿,具体怎么外包出去?", "哪些工作适合外包给外部团队而不是让AI干?", "为什么设计剪辑翻译这类重活要外包,而不是全靠AI?", "外部协作层主要包哪些活?能举几个实际例子吗?", "一人公司怎么把AI干不了的活儿交给别人做?", "(流水线工作流与运营闭环)", "什么叫流水线工作流?和运营闭环有啥区别?", "流水线工作流具体指哪几个环节?", "园区说的‘流水线工作流’到底是什么意思?能通俗讲讲吗?", "工作流是按步骤自动跑的吗?需要人工干预吗?", "流水线工作流和日常说的SOP是一回事吗?", "八、AI Agent(智能体)概念与一人公司应用\n- 标准工作流:需求输入、AI 拆解、AI 生成初稿、创始人审核微调、AI 优化排版、成品入库或上线。", "一人公司用AI的标准流程是怎样的?从接单到交货走几步?", "AI生成初稿后,创始人具体要审哪些地方?怎么微调?", "成品入库或上线前,AI优化排版都做些什么?", "需求输入之后,AI是怎么一步步拆解和生成的?", "标准工作流里哪几步必须人来把关?哪几步能全自动?", "八、AI Agent(智能体)概念与一人公司应用\n- 运营闭环:AI 批量产内容并多平台分发、AI 客服 7×24 响应并自动生成报价单、标准化 SOP 加 AI 自动交付、AI 记账报税。", "AI客服真能7×24小时回消息还自动生成报价单?靠谱吗?", "AI怎么实现多平台批量发内容?支持哪些平台?", "标准化SOP+AI自动交付,实际是怎么落地的?", "AI记账报税能做到什么程度?需要人复核吗?", "运营闭环包含哪几块?每块AI具体干了啥?", "八、AI Agent(智能体)概念与一人公司应用\n- 效果:启动成本可从传统创业的 20 万-100 万压缩到每月 500-5000 元;试错周期从半年缩短到 2-6 周。", "一人公司启动成本真能压到每月500块?都包含哪些开销?", "试错周期缩短到2-6周,是指从想法到验证结果只要这么短时间吗?", "相比传统创业,成本省在哪?500元和5000元档差别大吗?", "为什么AI能大幅压缩启动成本和试错时间?底层逻辑是啥?", "每月500-5000元这个成本范围,覆盖了哪些服务或工具?", "九、国内主要大模型厂商与选型\n- 国内大模型市场呈「三超多强」格局:第一梯队为豆包(字节)、通义千问(阿里)、DeepSeek;第二梯队为智谱 AI、文心一言(百度)、混元(腾讯);第三梯队为 Kimi、MiniMax 等创新企业。", "国内大模型厂商有哪些?怎么分梯队的?", "豆包、通义千问和DeepSeek是不是国内前三的大模型?", "国内大模型‘三超多强’具体指哪几家?", "字节、阿里、百度、腾讯各自的大模型叫什么名字?", "Kimi和MiniMax属于国内大模型的哪个梯队?", "九、国内主要大模型厂商与选型\n- 在简单问答、常识领域各大模型差异甚小,差异主要体现在复杂指令、多模态理解生成与多轮对话。", "大模型之间差别大吗?", "问答简单问题时,不同大模型效果差多少?", "为啥说复杂任务才看出大模型好坏?", "多模态和多轮对话能力哪家强?", "常识题大家都能答对,那真正比拼的是啥?", "九、国内主要大模型厂商与选型\n- DeepSeek:推理、代码、数学、性价比强,开源(MIT),API 定价低,适合写代码与高性价比推理。", "DeepSeek适合干啥?写代码行不行?", "DeepSeek开源吗?许可证是啥?", "DeepSeek API贵不贵?性价比怎么样?", "数学和推理任务用DeepSeek好不好?", "有没有开源又便宜、还擅长代码的大模型?", "九、国内主要大模型厂商与选型\n- 通义千问(阿里):通用、中文、企业云生态强,阿里云百炼 MaaS,长上下文,回答完整度突出。", "通义千问适合企业用吗?跟阿里云有啥关系?", "通义千问中文理解强不强?长文本处理怎么样?", "阿里云百炼平台支持通义千问吗?", "通义千问回答问题完整度高不高?", "企业上云选大模型,通义千问有啥优势?", "九、国内主要大模型厂商与选型\n- 豆包(字节):C 端应用、多模态、Agent,日活高、免费多模态,深度逻辑与代码能力相对弱。", "豆包能干啥?跟其他大模型比有啥特点?", "豆包免费的多模态功能具体包括哪些?", "豆包适合做智能体(Agent)开发吗?", "豆包在逻辑推理和编程方面表现如何?", "日活很高的豆包,主要面向C端还是B端用户?", "九、国内主要大模型厂商与选型\n- 智谱 GLM:编程、长程 Agent 任务强,开源、长上下文,政企私有化根基扎实。", "智谱GLM适合做编程任务吗?", "GLM模型在政企私有化部署方面表现怎么样?", "智谱的开源大模型支持长上下文吗?", "GLM做长程Agent任务效果如何?", "有没有适合私有化部署、又擅长写代码的国产大模型?", "九、国内主要大模型厂商与选型\n- 文心一言(百度):全模态、搜索、中文写作,功能完善度领先、合规资质最全。", "百度文心一言在中文写作方面强不强?", "文心一言支持图片和语音输入吗?", "哪家国产大模型的合规资质最全?", "搜索功能强、又能写公文的国产大模型是哪个?", "文心一言是不是全模态能力最强的国产模型?", "九、国内主要大模型厂商与选型\n- Kimi(月之暗面):超长文档、研报解析领先。", "Kimi能处理上百页的PDF研报吗?", "月之暗面的Kimi适合分析财报或行业报告吗?", "有没有特别擅长读长文档的国产大模型?", "Kimi在解析超长文本方面比其他国产模型强在哪?", "哪款国产模型看PDF、研报最准最细?", "九、国内主要大模型厂商与选型\n- 讯飞星火(科大讯飞):中文、语音交互、教育医疗,央国企与信创环境应用广。", "讯飞星火在教育场景用得多吗?", "科大讯飞的大模型语音识别和合成效果怎么样?", "央国企常用哪个国产大模型做信创适配?", "星火在医疗问诊辅助方面有实际落地案例吗?", "中文理解好、还能语音交互的国产模型是哪个?", "九、国内主要大模型厂商与选型\n- 混元(腾讯):社交、多模态、快慢思考,微信生态深度整合。", "混元大模型和微信打通了吗?", "腾讯混元支持图文生成和视频理解吗?", "混元有没有‘快思考’和‘慢思考’两种推理模式?", "社交场景下用得最多的国产大模型是哪个?", "哪个国产模型在微信生态里能直接调用AI能力?", "九、国内主要大模型厂商与选型\n- 写代码商用:智谱 GLM 或 DeepSeek;长文档处理:Kimi;多模态/免费 C 端:豆包;企业政企落地:智谱、文心一言或通义千问。", "写代码商用推荐哪个国产大模型?", "处理超长文档用哪个国内大模型最好?", "有没有免费又好用的国产多模态大模型?", "政企项目落地该选智谱、文心一言还是通义千问?", "做AI创业项目,国内大模型怎么选型?", "九、国内主要大模型厂商与选型\n- 各家价格口径不统一(有美元/人民币计、缓存命中与上下文位影响单价),比价务必回官网核对同一口径。", "国内各家大模型价格为什么不好直接比?", "比价时要注意哪些价格口径问题?", "美元计价和人民币计价的大模型服务怎么换算对比?", "缓存命中率会影响大模型使用单价吗?", "上下文长度不同,大模型收费会变吗?", "九、国内主要大模型厂商与选型\n- 首期成效:受理 58 个项目,23 个成功入驻,覆盖人工智能、低空经济、跨境服务等领域。", "昆明大学生创业园首批入驻了多少个项目?", "目前有多少项目成功入驻园区?", "园区首期覆盖了哪些产业领域?", "人工智能、低空经济这些方向在园区有项目落地吗?", "园区第一批受理的58个项目里,最终入驻率是多少?", "> 数据口径声明:本知识库内容全部依据《昆明市创业园宣传册》《昆明市大学生创业园入驻企业信息表》《昆明市大学生创业园运行情况统计表(2026年7月)》《附件1.省级创业孵化载体相关情况简介材料》《昆明市大学生创业园创业企业(项目)入驻管理规程》等标准资料整理;涉及运营数据时严格区分「官方统计口径」「省级绩效累计口径」「AI 平台运行指标」三类口径,不得混用。", "这份知识库的数据来源是哪些文件?", "运营数据里的‘官方统计口径’和‘省级绩效累计口径’有啥区别?", "AI平台运行指标能和园区官方统计数据混着用吗?", "知识库里提到的三类数据口径分别指什么?", "《昆明市大学生创业园创业企业(项目)入驻管理规程》在知识库中起什么作用?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 全称:昆明市大学生创业园(KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK)", "昆明市大学生创业园的英文全称是什么?", "这个园区的正式名称叫什么?", "昆明大学生创业园是不是就叫‘昆明市大学生创业园’?", "KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK 对应的中文名是啥?", "园区全称里带不带‘市’字?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 性质:云南省首家由政府主办、免费为创业青年大学生提供创业孵化服务的开放式园区", "云南首家政府办的大学生创业孵化园区是哪个?", "有没有免费给大学生创业用的政府园区?", "昆明有没有专为青年大学生开的创业孵化基地?", "这个园区是政府主办的吗?对大学生收费吗?", "听说有个开放式创业园区专门服务大学生,是真的吗?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 成立时间:2009 年(2009 年 9 月成立)", "这个园区是哪一年成立的?", "昆明市大学生创业园什么时候开始运营的?", "2009年9月成立的创业孵化基地是哪个?", "园区创办至今有多少年了?", "最早一批大学生创业孵化载体是什么时候建的?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 主管部门:昆明市人力资源和社会保障局", "昆明大学生创业园归哪个部门管?", "谁在负责管理这个创业孵化园区?", "昆明市人社局是不是这个园区的主管单位?", "园区的上级主管部门是人社局吗?", "这个创业园是昆明市人社局直接办的吗?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 园区地址:昆明市民航路 229 号(昆明市人力资源中心 1—2 楼)", "昆明大学生创业园具体在哪儿?", "园区地址是昆明市民航路229号吗?", "怎么去昆明市人力资源中心1-2楼的创业园?", "在昆明市民航路附近有个创业孵化基地,是不是就是这个?", "这个园区和昆明市人力资源中心是一个地方吗?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 建筑面积:约 3000 平方米,可容纳约 50 个大学生创业团队入驻", "园区大概有多大面积?", "能同时容纳多少个大学生创业团队?", "3000平米的创业空间能入驻多少支队伍?", "听说最多能支持50个团队,是真的吗?", "园区的物理空间和承载能力怎么样?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 主导产业:数字经济与电子信息产业", "园区主要发展哪些产业?", "这个创业园重点支持什么类型的创业项目?", "园区的主导产业是数字经济和电子信息吗?", "你们园区聚焦在哪些行业领域?", "创业孵化主要面向数字经济和电子信息企业吗?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 认定省级创业园时间:2012 年", "园区是什么时候被评为省级创业园的?", "园区获得省级创业孵化载体认定是哪一年?", "2012年是不是园区被认定为省级创业园的时间?", "园区成为省级创业园有多久了?", "省级创业园资质是哪年拿到的?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 咨询电话:18087174891 / 17787129131 / 18214573650;接收邮箱:714066050@qq.com", "园区的联系电话是多少?", "我该打哪个电话咨询入园事宜?", "能发一下园区的客服电话和邮箱吗?", "联系园区工作人员用什么方式?", "园区接收咨询的邮箱是714066050@qq.com吗?", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 服务模式:以「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体全链条创业扶持体系,打造「一站式」创业孵化服务平台", "园区提供哪些创业服务?", "什么是‘七位一体’创业扶持体系?", "园区说的‘一站式’孵化服务具体包括哪些内容?", "AI赋能是园区服务的一部分吗?", "空间、孵化、融资、政策这些服务都包含在园区服务体系里吗?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 实际入驻企业数:38 家(2026 年 7 月报送当月数)", "现在园区里有多少家企业在运营?", "截至2026年7月,园区入驻企业总数是多少?", "38家是园区当前的实际入驻企业数量吗?", "最新统计的在园企业数是38家吗?", "2026年7月的数据说园区有38家企业入驻,对吗?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 园区可容纳企业(实体)个数:49 个", "园区最多能容纳多少家企业?", "这个园区一共能入驻几家单位?", "园区的总企业容量是多少?", "园区最多可以放几家公司?", "园区的企业承载上限是几个?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 开园以来累计投入园区运营资金:560 万元", "园区开园到现在总共花了多少钱运营?", "到目前为止,园区运营投入了多少钱?", "园区累计花了多少运营经费?", "从开园到现在,园区运营资金总共用了多少?", "园区运营至今一共投入了多少万元?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计带动就业人数(当年累计):213 人", "现在园区里的企业一共招了多少人就业?", "入驻企业今年加起来带动了多少人就业?", "园区企业今年提供了多少个就业岗位?", "目前园区企业累计解决了多少人的就业?", "园区企业今年一共吸纳了多少就业人员?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计生产经营总额(当年累计):252.15 万元", "园区企业今年一共赚了多少钱?", "入驻企业今年的生产经营总收入有多少?", "园区企业今年的总营收是多少?", "所有入驻企业今年合计做了多少营业额?", "园区企业今年生产经营总额达到多少万?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计上缴税利总额(当年累计):1.03 万元", "园区企业今年一共交了多少税和利润?", "入驻企业今年上缴的税利总额是多少?", "园区企业今年给政府交了多少钱(含税和利润)?", "今年园区企业累计上缴税利总共多少?", "园区企业今年的税利总额有多少钱?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 开园以来累计成功孵化出园企业数:149 家", "园区开园到现在一共孵化成功并毕业的企业有多少家?", "从开园到现在,有多少家企业已经成功出园了?", "园区累计成功孵化并出园的企业总数是多少?", "截止目前,园区累计有多少家企业顺利毕业?", "开园以来,园区一共送走了多少家孵化成功的企业?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 当年开展活动场次:11 场;当年培训人数:362 人", "今年园区一共办了多少场活动?", "2026年园区组织了多少次培训?", "今年园区开展了多少场活动,培训了多少人?", "今年园区的活动场次和培训人数分别是多少?", "2026年园区总共培训了多少人次?办了多少场活动?", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 备注:统计表 3、9 栏为开园以来累计数,5 栏为报送当月实际入驻数,其余为当年累计数", "统计表里哪些数据是累计的、哪些是当月的、哪些是当年的?", "《2026年7月运行情况统计表》中第3栏和第9栏代表什么?第5栏呢?", "园区统计表里的数据怎么区分是累计、当月还是当年的?", "统计表中各栏数据的统计口径分别是什么?", "怎么看懂园区统计表里不同栏位的数据含义?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 认定为省级创业孵化载体以来累计孵化企业:181 家;当前实有在孵创业实体:31 家", "省级创业孵化载体认定以来,园区一共孵化了多少家企业?", "截至2026年5月,园区累计孵化企业数是多少?", "园区被评上省级孵化载体后,总共孵化了多少家企业?", "按省级绩效口径算,园区累计孵化企业总数是多少?", "截至今年5月,园区在省级认定下累计孵化了多少家企业?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 团队(实体)出园数:累计 150 家(本年度 11 家)", "园区到目前为止一共送走多少家团队或企业?", "省级口径下,园区累计出园的创业实体有多少家?", "2026年这一年园区有多少家团队(实体)出园了?", "截至2026年5月,园区累计和本年度的出园数分别是多少?", "园区累计出园企业和今年新出园的企业各有多少家?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队发明专利数:累计 171 项(本年度 13 项)", "园区入驻团队总共申请了多少项发明专利?", "截至2026年5月,园区累计有多少件发明专利?", "今年新申请的发明专利有多少项?", "入驻团队的专利总数是多少?", "园区孵化企业一共拿了多少个发明专利?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队累计经营收入:21,911.14 万元(约 2.19 亿元)", "园区入驻团队到现在一共赚了多少钱?", "截至2026年5月,入驻企业的总经营收入有多少?", "累计经营收入大概多少个亿?", "园区企业总共营收多少万?", "现在所有入驻团队加起来收入有2.19亿吗?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队累计税收缴纳:426.2 万元", "园区企业一共交了多少税?", "截至2026年5月,累计缴税金额是多少?", "入驻团队总共纳税426.2万元对吗?", "园区孵化企业合计缴了多少税款?", "税收缴纳总额接近430万了吗?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 累计带动就业:1,529 人(本年度 106 人)", "园区目前带动了多少人就业?", "累计带动就业人数有多少?", "今年新增带动就业多少人?", "1529人是包括今年新招的106人吗?", "园区总共帮多少人找到了工作?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 创业指导专家团队:50 人;运营团队:3 人", "园区创业指导专家一共有多少人?", "运营团队有几个人?", "专家团队和运营人员各有多少?", "创业导师队伍规模多大?", "园区有50个专家加3个运营人员对吗?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 开展创业指导服务活动:累计 83 场(本年度 18 场)", "园区一共办过多少场创业指导服务活动?", "截至2026年5月,园区累计开展了多少场创业指导活动?", "今年(2026年)园区办了多少场创业指导活动?", "创业指导服务活动总共83场,其中今年有几场?", "园区创业指导活动历年累计和本年度分别是多少场?", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 近 3 年入孵实体孵化成功率:95.91%", "园区近3年孵化成功的创业企业比例是多少?", "入孵企业的孵化成功率有多少?", "过去三年里,园区孵化成功的比例是95.91%吗?", "孵化成功率95.91%是指最近三年的吗?", "园区近三年的孵化成功率具体是多少?", "四、入驻对象与条件(依据《入驻管理规程》)\n- 入驻对象范围:在校大学生及毕业 8 年内的高校毕业生、归国留学生、外籍留学生等青年创业企业(项目);申请人须担任企业法定代表人或项目负责人;对符合昆明市产业发展的优质创业企业(项目)或 OPC 企业可适当放宽入驻条件", "哪些人可以申请入驻园区?", "在校大学生能申请入驻吗?毕业几年内的毕业生可以?", "留学生包括归国和外籍的都能申请吗?", "不是法定代表人,只是项目负责人能入驻吗?", "如果公司很优质但申请人不是法人,还能放宽入驻吗?", "四、入驻对象与条件(依据《入驻管理规程》)\n- 产业方向:符合国家产业政策,聚焦昆明市「10+N」产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等)", "园区重点支持哪些产业方向的创业项目?", "昆明市‘10+N’产业链具体包括哪些?", "做数字经济或生物医药的项目符合入驻产业要求吗?", "绿色食品、文化旅游这些算园区支持的产业吗?", "我的项目属于新能源领域,符不符合园区的产业定位?", "四、入驻对象与条件(依据《入驻管理规程》)\n- 其他要求:有明确可行的经营计划、启动资金和风险承担能力,具备规范的企业管理制度;无知识产权纠纷、无违法犯罪记录、无经营异常记录", "入驻园区对企业经营计划和资金有什么要求?", "申请入驻需要提供详细的经营计划书吗?", "启动资金有没有最低金额要求?", "企业管理制度要规范到什么程度?", "有知识产权纠纷或者经营异常记录还能申请入驻吗?", "四、入驻对象与条件(依据《入驻管理规程》)\n- 优先入驻情形:获得市级及以上创业大赛奖项、拥有突出学术成就、重大发明创造及高新专利技术的项目", "哪些创业项目能优先入驻园区?", "拿过市级以上创业比赛奖的项目是不是更容易进园区?", "有发明专利或者高学历的团队,入驻园区有没有优待?", "学术成果特别突出的创业者,申请园区入驻会加分吗?", "园区对拥有高新技术专利的项目,入驻时有什么特殊政策?", "园区原则上每年组织两次入驻工作,可适时增减调整。流程如下:", "园区一年组织几次入驻申请?", "入驻申请是固定时间还是随时都能提交?", "如果错过了常规申请时间,还能不能补报?", "园区入驻工作每年安排几次?会不会临时加开?", "入驻申请的时间安排是固定的吗?能不能根据情况调整?", "五、入驻流程(依据《入驻管理规程》)\n1. 提出申请:提交《入驻申请表》《创业计划书》、创业者身份证(外籍提供护照、签证)、学历证明、企业营业执照(已注册企业)、相关资质专利证书等材料", "申请入驻园区需要准备哪些材料?", "第一次申请园区入驻,要交哪些表格和证明?", "还没注册公司,只有一份创业计划书,能申请入驻吗?", "外籍创业者申请园区入驻,身份证件要提供什么?", "提交入驻申请时,专利证书和学历证这些必须一起交吗?", "五、入驻流程(依据《入驻管理规程》)\n2. 项目初审:服务机构对申请材料初审,材料不齐的一次性告知补全", "材料不全的话,园区会怎么处理我的入驻申请?", "初审没通过是因为材料少了,还能补交吗?", "服务机构初审时发现材料缺东西,会告诉我补什么吗?", "提交材料后被告知不齐全,是不是还有机会补正?", "初审阶段材料有问题,是一次性告知还是分批提醒?", "五、入驻流程(依据《入驻管理规程》)\n3. 入驻评审:从《昆明市就业创业导师库》抽取 3 名评审专家,采取路演答辩方式评审,实行量化打分制(总分 100 分,80 分及以上准予通过)。评分维度:团队资质与能力(20 分)、项目创新性与技术壁垒(25 分)、产业适配与市场前景(25 分)、运营规划与落地能力(15 分)、社会价值与发展潜力(15 分)", "入驻评审是怎么进行的?要答辩吗?", "路演答辩具体怎么操作?有没有评分标准?", "评审专家从哪里来?几个人打分?多少分算通过?", "入驻评审总分100分,80分以上才合格,那各项分别占多少分?", "团队实力、项目创新性、市场前景这些在评审里各占多少比重?", "五、入驻流程(依据《入驻管理规程》)\n4. 评审公示:报主管部门备案后,在昆明市人力资源和社会保障局官方网站公示,公示无异议的准予入驻", "公示是在哪个网站上进行的?", "公示完没异议是不是就代表能入驻了?", "公示阶段需要我本人做什么吗?", "公示期是多长时间?如果有人提异议怎么办?", "公示结果在哪里查?人社局官网具体哪个栏目?", "五、入驻流程(依据《入驻管理规程》)\n5. 签署协议:签订《入驻孵化协议》,分配办公场地/工位,正式入驻", "签完协议后多久能拿到工位?", "《入驻孵化协议》里都写了哪些关键内容?", "签协议的时候要带什么材料?", "办公场地是随机分配还是可以自己选?", "签了协议但还没拿到工位,算不算正式入驻?", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- OPC 创业空间:面向以人工智能技术为支撑、聚焦数字经济/AI 应用/软件开发/文创设计/电商直播等轻资产领域的项目,提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年", "OPC创业空间适合做AI小程序的团队吗?", "做电商直播的项目能申请OPC的独立工位吗?", "文创设计类项目在OPC最多能待几年?", "OPC是直接就能搬进去办公,不用装修吧?", "软件开发和AI应用类项目,OPC和其他区域有啥区别?", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 创业苗圃区:面向创意阶段、未注册企业的大学生创业团队,提供开放式共享工位,孵化周期最长 2 年;成长好的项目经评审可推荐至孵化加速区孵化 1 年", "还没注册公司,但有个创业点子,能进园区吗?", "大学生创业团队只能进创业苗圃区吗?", "在苗圃区表现好,怎么才能去加速区?", "创业苗圃区给的是不是那种开放大办公室?", "苗圃区最多待两年,之后必须搬走吗?有没有其他出路?", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 孵化加速区:面向已完成工商注册、处于初创期的企业,提供独立办公空间,基础孵化周期 2 年,期满经评估合格可申请延期 1 年,累计不超过 3 年", "已经注册了公司,但刚成立三个月,能进孵化加速区吗?", "加速区的2年孵化期满了还能续吗?怎么申请延期?", "独立办公空间是带家具和网络的吗?", "加速区对企业有没有营收或人员规模要求?", "总共最多能在加速区待三年,这三年里要每年评估吗?", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 国际创客区:面向归国留学生、外籍留学生创业项目,提供个性化办公空间及国际化配套服务,孵化周期参照孵化加速区执行", "国际创客区是给哪些人用的?", "留学生创业能进国际创客区吗?", "外籍学生创业有专门的孵化区域吗?", "归国留学生创业有啥特别支持?", "国际创客区的孵化时间是多久?", "七、政策与孵化服务(依据《宣传册》)\n- 空间保障:免费提供办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等", "园区给创业团队提供免费办公场地吗?", "工位和独立办公室都免费吗?", "办公桌椅、网络这些基础设备园区包吗?", "新入驻团队能拿到哪些硬件支持?", "有没有免费的高速网络和文件柜?", "七、政策与孵化服务(依据《宣传册》)\n- 孵化服务:创业指导专家一对一辅导,定期举办创业沙龙、创业讲堂、项目路演等", "创业初期有老师一对一指导吗?", "园区会组织创业相关的活动吗?", "有没有创业沙龙或者路演机会?", "创业讲堂多久办一次?参加要收费吗?", "刚起步的项目能获得哪些辅导服务?", "七、政策与孵化服务(依据《宣传册》)\n- 融资支持:联动省内外创业投资机构,提供政策性贷款、商业银行贷款、股权融资等推荐指导", "园区能帮忙找投资吗?", "想贷款创业,园区能推荐银行或政策性贷款吗?", "股权融资的事园区管不管?", "有没有对接风投机构的服务?", "融资这块园区具体怎么帮创业者?", "七、政策与孵化服务(依据《宣传册》)\n- 政策对接:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策", "园区能帮我们申请创业补贴吗?", "税收减免和社保补贴怎么弄?", "高校毕业生创业有哪些专属政策?", "创业扶持资金申报流程园区会协助吗?", "政策对接服务包括哪些具体内容?", "七、政策与孵化服务(依据《宣传册》)\n- 资源整合:对接驻昆高校、科研院所、龙头企业,纳入昆明创业创新服务体系,优先参与「春城创业荟」等品牌活动", "园区能帮我们对接哪些高校和科研资源?", "能不能推荐我们参加春城创业荟这类活动?", "园区怎么帮企业融入昆明的创新创业服务体系?", "入驻后能对接昆明本地龙头企业吗?", "园区有没有和驻昆高校合作的资源?", "七、政策与孵化服务(依据《宣传册》)\n- 专项赋能培训:定期开展 AI 公益性培训(AI 技术应用、数字内容创作、一人公司创业落地、企业 AI 降本增效)", "园区提供哪些免费的AI培训课程?", "有没有教怎么用AI做内容创作的培训?", "想学AI怎么帮公司省钱增效,园区有相关培训吗?", "一个人创业,园区有专门的AI落地指导吗?", "AI公益培训都包括哪些具体内容?", "七、政策与孵化服务(依据《宣传册》)\n- 其他服务:工商注册、税务代办、知识产权咨询、法律援助等", "园区能帮忙办营业执照吗?", "税务申报这些事园区能代办吗?", "知识产权方面有问题,园区能给咨询建议吗?", "遇到法律纠纷,园区提供法律援助吗?", "工商、税务、法务、知产这些基础服务园区都包办吗?", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 年度考核:量化打分制(总分 100 分),维度为经营发展能力(20)、技术创新能力(25)、合规与履约情况(20)、创业带动成效(20)、发展潜力与孵化成效(15);等级:优秀(80 分及以上)、合格(60—79 分)、不合格(60 分以下)", "园区每年怎么考核入驻企业?", "考核满分多少分?多少分算合格?", "考核主要看哪几个方面?各自占多少分?", "经营发展能力和技术创新能力分别占多少分?", "考核等级分几档?每档的分数线是多少?", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 退出类型:成功孵化出园、孵化期满退出、主动申请退出、强制勒令退出", "企业什么情况下要离开园区?", "孵化成功了是不是就得搬走?", "如果自己不想继续入驻了能主动退出吗?", "什么情况会被园区强制要求退出?", "孵化期满了还没毕业,还能继续待吗?", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 强制退出情形:累计 30 个工作日无人到现场办公、连续 7 个工作日场地空置且未报备累计超 3 次、给园区造成重大经济损失或名誉损失、不服从管理情节严重、被吊销执照或破产清算、违法违规经营、私自转租或虚报数据等", "如果公司连续好几天没人来办公室办公,会不会被园区赶出去?", "场地空着没用又没提前报备,几次会被强制清退?", "什么情况下园区会直接让企业搬走?比如欠钱、违法或者乱转租行不行?", "不服从园区管理到什么程度算严重?会被踢出去吗?", "公司执照被吊销了,是不是必须马上退出园区?", "九、OPC 概念(一人公司创业模式)\n- OPC 即 One Person Company「一人公司」创业模式:以人工智能技术为支撑,聚焦数字经济、AI 应用、软件开发、文创设计、电商直播等轻资产领域,一人即可启动运营的创业形态", "啥叫OPC创业模式?是不是一个人就能开公司?", "AI一人公司到底是什么意思?靠人工智能怎么单干创业?", "做电商直播或者AI应用,一个人能注册公司并在园区起步吗?", "OPC适合哪些行业?搞软件开发或文创设计能行吗?", "轻资产创业是不是就指不用租大办公室、一个人就能干的活?", "九、OPC 概念(一人公司创业模式)\n- 园区 OPC 创业空间为 OPC 创业者提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年;园区定期开展「AI 一人公司实战课」等专项培训", "OPC创业者在园区能租到独立工位吗?最长能待多久?", "园区给一人公司提供共享办公位吗?有没有拎包入住那种?", "孵化期最多两年,是从签合同那天开始算,还是入驻当天起算?", "园区有没有专门教一个人怎么用AI创业的培训课?叫啥名字?", "‘AI一人公司实战课’多久开一次?新人能报名参加吗?", "九、OPC 概念(一人公司创业模式)\n- 云南首个人工智能 OPC 创新人才基地落地昆明(2026 年 3 月公开报道),填补省内个体 AI 创业培育空白", "云南第一个AI一人公司基地在哪?什么时候建好的?", "昆明是不是全省首个搞AI个体创业培育的地方?有官方消息吗?", "2026年3月说的云南首个人工智能OPC人才基地,具体在昆明哪个位置?", "这个AI OPC基地是政府牵头还是园区自己建的?", "之前云南没有专门培养AI个体创业者的基地,对吗?", "在园企业以《2026年7月运行情况统计表》官方口径 38 家为准;《入驻企业信息表》收录本期入驻企业(项目)名录 39 家,含云南派音人工智能科技、米勒克尔蓝宝石珠宝产业化、中泰研学合作、云南宸中低空经济、昆明智海银高文化科技、人工智能机器人大模型训练、云南廷秀文旅康养、瀚颖 AI+ 教育咨询、云南大学 AI+ 创业平台、仰光客厅、云南上古绝学文化、千楣之路、中越生物医疗、中越文旅交流、绿态环保科技、联翼航空低空经济、酷享野农 AI 农业、滇缅国际设计、昆明舒诺生物科技、", "现在园区里到底有多少家企业?38家还是39家?为啥数字不一样?", "《运行情况统计表》和《入驻企业信息表》哪个才是最新最准的名单?", "云南派音、智海银高、廷秀文旅这些公司都在园区里办公吗?", "名单里有‘人工智能机器人大模型训练’这种项目,是真实入驻的企业吗?", "中越、滇缅、仰光客厅这些带国际字样的项目,也是园区的正式入驻单位?", "达岸教育管理、昆明滇油石化、花仙子园艺肥料、Facebook 越南茶器跨境、研X 同行者网络、鬼才明 AI 创意工作室、朵哈·玫瑰特色产业链、昆明云韵体育、昆明舒护安养老服务、南菌优培食用菌、五华区丽裳文化、云南星瑞航空、云南直播私域平台、昆明屿澈电商、中外青少年研学、星杭商务信息咨询、蓝智科技、云品出滇·纸享万家、云迹轻旅、启元人工智能科技。行业分布(按简介分类):人工智能 7 家、跨境电商 5 家、教育培训 4 家、文旅康养 4 家、文化创意 4 家、生物医药 4 家、", "园区里有哪些企业?能按行业分类列出来吗?", "达岸教育、昆明滇油石化这些公司都在园区里吗?它们分别属于什么行业?", "人工智能和跨境电商类的企业在园区里各有多少家?", "园区里做文旅康养和生物医药的企业有哪些?", "云南星瑞航空和昆明云韵体育是不是也在这个园区?它们属于哪个行业类别?", "低空经济 3 家、现代农业 3 家、绿色环保 2 家、企业服务 2 家、新材料 1 家", "低空经济相关的企业在园区有几家?", "园区里搞现代农业的公司有哪几个?", "绿色环保和新材料方向的企业各有多少家?", "企业服务类的企业在园区有几家?都叫什么名字?", "低空经济、现代农业、绿色环保这些新兴领域在园区的布局情况是怎样的?", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- TOKEN 消耗:月均约 200 亿 tokens(折合日均约 6.67 亿 tokens、实时速率约 7,700 t/s)——AI 平台运行锚点测算", "AI平台每个月大概消耗多少token?", "平台每天平均用多少token?每秒处理量大概是多少?", "200亿tokens每月这个数据是官方统计的还是内部测算的?", "AI平台的token消耗量是怎么算出来的?有没有日均或实时速率参考?", "6.67亿tokens日均和7700t/s实时速率这两个数字准确吗?来源是哪里?", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- 工具调用:按平均每次工具调用约 15 万 tokens 折算,与 TOKEN 消耗实时联动", "每次调用工具大概消耗多少token?", "工具调用次数和token消耗之间是怎么换算的?", "15万tokens/次这个平均值是所有工具都一样吗?", "工具调用和token消耗是实时联动的,具体怎么联动的?", "如果调用次数变多了,token消耗会同比例增加吗?", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- 实时运营数据请调用 get_park_overview 工具获取,以工具返回为准", "怎么查园区最新的实时运营数据?", "get_park_overview这个工具要怎么调用?在哪里用?", "园区的实时数据以什么为准?是网页显示的还是必须调工具?", "调用get_park_overview能拿到哪些具体指标?", "实时运营数据更新频率是多少?调一次能获取多长时间范围的数据?", "常见问题\n- 问:如何入驻园区?答:提交申请(入驻申请表、创业计划书、身份证、学历证明等)→ 项目初审 → 入驻评审(导师库抽 3 名专家、路演答辩、80 分通过)→ 评审公示(市人社局官网)→ 签订《入驻孵化协议》、安排场地工位;每年原则上两次。", "我想申请入驻园区,具体要准备哪些材料?流程是怎样的?需要答辩吗?多少分才算通过?", "入驻昆明市大学生创业园要走什么流程?提交材料后多久能知道结果?每年有几次申请机会?", "创业项目怎么才能进园区?是不是要先交计划书和身份证?评审环节是几个人打分?最低多少分能过?", "园区入驻要面试或路演吗?评审专家从哪来?公示是在哪里看?签协议前还要做什么?", "新人怎么申请入园?需要学历证明吗?评审时是线上还是线下?通过标准是80分固定分吗?", "常见问题\n- 问:园区有哪些政策扶持?答:空间免费、孵化辅导、融资对接、政策申报、资源整合、AI 专项培训、工商税务法律一站式服务(详见「政策与孵化服务」)。", "园区给创业公司提供哪些免费或补贴类支持?比如办公室、培训、找投资这些有没有?", "在园区创业能享受哪些实际帮助?像办公场地、法律咨询、AI培训这些包不包括?", "听说有‘一站式’服务,具体包含工商、税务、法律哪些内容?AI专项培训是定期开班吗?", "除了免租金,园区还帮企业对接融资、申报政策吗?辅导是免费的吗?", "园区有哪些孵化服务?空间免费是真的吗?政策申报是指帮我们申请政府补贴吗?", "常见问题\n- 问:园区有多少家企业、带动多少就业?答:按官方统计(2026 年 7 月)实际入驻 38 家、当年累计带动就业 213 人;省级绩效累计口径(截至 2026 年 5 月)累计带动就业 1,529 人、累计经营收入约 2.19 亿元。请说明所需口径。", "现在园区里有多少家在孵企业?总共带动了多少人就业?是按年度算还是累计算?", "2026年7月的数据说有38家企业,那带动就业213人是指这38家今年新招的吗?1529人又是怎么算出来的?", "园区就业数据有两个口径,到底该用哪个?比如写汇报材料时该引用哪个数字?", "‘累计带动就业1529人’是截至什么时候的?经营收入2.19亿是所有企业的总和吗?", "园区目前实际在运营的企业数量是多少?最新就业带动人数怎么看?不同统计口径的区别在哪?", "常见问题\n- 问:什么是 OPC?答:一人公司(One Person Company)创业模式,详见「OPC 概念」。", "OPC是什么意思?是不是就是一个人开的公司?和普通个体户有啥区别?", "听说园区支持OPC创业模式,这个‘一人公司’具体指什么?需要几个人注册?", "OPC是昆明园区特有的吗?和有限责任公司比,注册和运营上有什么不同?", "想一个人创业,能注册OPC吗?园区对OPC有没有额外扶持?", "‘One Person Company’翻译成中文就是一人公司吗?在园区里怎么申请这种类型?", "> 整理自 `数据资料/` 目录全部原始材料:\n> 附件1.省级创业孵化载体相关情况简介材料.xlsx · 昆明市大学生创业园入驻企业信息表.xlsx · 昆明市大学生创业园运行情况统计表(2026年7月).xls · 昆明市大学生创业园创业企业(项目)入驻管理规程.wps · 昆明市创业园宣传册.pdf · 空间布局.png / 详细的布局.png · 昆明市大学生创业园 定稿(品牌设计稿)\n>\n> 本文件尽量保留原始表格、文件中的真实数值与原文字,供对外申报、总结汇报、", "园区的原始资料和数据都存在哪里?比如入驻企业名单、运行统计表这些文件能公开查吗?", "对外申报或写总结报告时,该参考哪些权威文件?比如Excel表格和PDF宣传册哪个更准?", "‘数据资料/’目录里的几个文件,比如入驻企业信息表和运行情况统计表,最新更新到什么时候?", "品牌设计稿和空间布局图这些视觉材料,能用于对外宣传吗?有没有使用规范?", "园区所有原始材料都在‘数据资料/’文件夹里吗?比如管理规程和宣传册这些,是不是最新定稿版本?", "宣传推广等使用。\n---", "宣传推广可以用这个知识库里的内容吗?", "我能不能拿这里面的信息去做园区宣传?", "知识库里哪些内容适合对外宣传用?", "做推广材料时能直接引用这里的资料吗?", "宣传推广相关的工作可以参考这个知识库吗?", "目录\n1. [园区基本信息](#1-园区基本信息)", "园区的基本情况都在哪查?", "我想了解园区的基本信息,该看哪部分?", "园区基本信息包括哪些内容?在目录里怎么找?", "哪里能找到园区的概况、地址、面积这些基础资料?", "目录里标着‘园区基本信息’的是第几项?", "目录\n2. [运营绩效核心指标(省级申报口径)](#2-运营绩效核心指标省级申报口径)", "运营绩效的核心指标有哪些?", "省级申报要求的运营绩效指标是哪些?", "申报省级园区时要看哪些关键绩效数据?", "运营绩效指标按省级标准是怎么定的?", "目录里‘运营绩效核心指标(省级申报口径)’具体指什么?", "目录\n3. [2026年1—7月运行情况月统计](#3-2026年17月运行情况月统计)", "2026年1到7月的运行数据有吗?", "每月的运行统计表在哪找?", "有没有2026年前七个月的月度运行情况汇总?", "运行情况的月度统计更新到几月份了?", "目录里说的‘2026年1—7月运行情况月统计’包含哪些内容?", "目录\n4. [入驻企业完整名录(39家/信息表)](#4-入驻企业完整名录39家信息表)", "入驻企业名单在哪里看?", "一共有多少家企业入驻园区?", "能查到所有入驻企业的详细信息吗?", "39家入驻企业的信息表怎么获取?", "目录里‘入驻企业完整名录(39家/信息表)’包含联系方式和业务范围吗?", "目录\n- [A. 孵化加速区(加速1–10)](#a-孵化加速区)", "孵化加速区在哪儿?", "加速1到10号房间属于哪个区域?", "园区里叫‘孵化加速区’的地方是哪一块?", "我想去孵化加速区,怎么找?", "加速1–10这些办公室是不是都在孵化加速区?", "目录\n- [B. 国际创客区(国际1–7)](#b-国际创客区)", "国际创客区具体在什么位置?", "国际1到7号房间属于哪个功能区?", "园区里有国际创客区吗?在哪?", "国际创客区是不是专门给外国创业者用的?", "国际1–7这些工位是在国际创客区吗?", "目录\n- [C. 成长区(成长1–8)](#c-成长区)", "成长区包括哪些房间号?", "成长1到8号房间归哪个区域管?", "园区的成长区在几号楼?怎么走?", "创业公司发展到一定阶段能进成长区吗?", "成长1–8这些空间是不是叫成长区?", "目录\n- [D. OPC创业空间](#d-opc创业空间)", "OPC创业空间是独立办公区吗?", "OPC创业空间有没有固定房间号?", "园区里提到的OPC创业空间是指哪个地方?", "OPC创业空间和别的区域比如孵化加速区是一样的吗?", "OPC创业空间是对外开放申请的吗?", "目录\n- [E. 一楼直播间](#e-一楼直播间)", "一楼直播间在园区哪一层?", "园区的一楼直播间具体在哪个位置?", "想用直播间得去一楼吗?", "E区就是一楼直播间对吧?", "一楼直播间是编号E吗?开放给谁用?", "目录\n5. [省级申报·入驻实体名单(31家)](#5-省级申报入驻实体名单31家)", "园区里有哪些已经成功入驻的省级申报企业?", "能给我看看省级申报的31家入驻实体名单吗?", "哪些企业是通过省级申报进园区的?", "省级申报入驻的企业一共有多少家?", "有没有省级申报入驻企业的详细名单?", "目录\n6. [企业(项目)入驻管理规程要点](#6-企业项目入驻管理规程要点)", "企业入驻园区需要遵守哪些管理规定?", "园区对企业(项目)入驻有哪些管理要求?", "入驻后园区怎么管理企业和项目?", "企业入驻管理规程主要讲了哪些内容?", "园区对企业入驻后的日常管理有哪些要点?", "目录\n7. [空间布局](#7-空间布局)", "园区的整体空间是怎么规划的?", "园区内部的空间布局是怎样的?", "能介绍一下园区各功能区域的分布情况吗?", "园区有哪些主要的功能分区?", "园区的物理空间结构或场地划分是怎样的?", "目录\n8. [宣传册核心内容(园区简介·服务·政策)](#8-宣传册核心内容园区简介服务政策)", "园区宣传册里都写了些什么内容?", "宣传册里关于园区简介、服务和政策的部分具体包括哪些?", "想快速了解园区,宣传册的核心信息有哪些?", "园区宣传册重点介绍了哪些方面?", "宣传册里关于服务和扶持政策的内容有哪些?", "目录\n9. [入驻条件与入驻流程](#9-入驻条件与入驻流程)", "企业要满足什么条件才能入驻园区?", "新企业入驻园区的具体流程是怎样的?", "从申请到正式入驻园区,一共要走哪些步骤?", "入驻园区需要符合哪些硬性条件?", "入园的门槛和办理流程分别是什么?", "目录\n10. [孵化周期(四大孵化区域)](#10-孵化周期四大孵化区域)", "孵化周期是多久?", "园区有哪几个孵化区域?", "四大孵化区域分别叫什么名字?", "不同孵化区域的周期一样吗?", "孵化时间最长能有多久?", "目录\n11. [报名方式](#11-报名方式)", "怎么报名入驻园区?", "报名需要走什么流程?", "申请入园要提交哪些材料?", "线上还是线下报名?", "报名入口在哪里?", "目录\n12. [媒体报道与合作媒体](#12-媒体报道与合作媒体)", "园区上过哪些媒体报道?", "有哪些媒体和园区合作过?", "园区被哪些新闻平台报道过?", "合作媒体名单能看看吗?", "有没有主流媒体对园区的采访或专题?", "目录\n13. [园区新闻与活动大事记](#13-园区新闻与活动大事记)", "园区最近有什么新闻?", "园区举办过哪些活动?", "有没有园区的年度大事记?", "想了解园区近期动态,从哪看?", "园区过去一年都做了哪些重要事情?", "目录\n14. [品牌VI设计规范](#14-品牌vi设计规范)", "园区的品牌设计规范在哪查?", "VI手册包含哪些内容?", "Logo、字体、颜色这些怎么用才合规?", "品牌视觉识别标准有官方文件吗?", "对外宣传时VI使用要注意什么?", "| 项目 | 内容 |\n|---|---|\n| 载体名称 | 昆明市大学生创业园 |\n| 载体类别 | 省级创业园(2012年获评) |\n| 载体地址 | 云南省昆明市官渡区民航路229号(昆明市人力资源中心二楼) |\n| 成立时间 | 2009年9月(2026年4月全面优化后“焕新亮相”) |\n| 主管部门 | 昆明市人力资源和社会保障局 |\n| 运营机构 | 云南汇浙青创园区投资有限公司(统一社会信用代码 91530112MA6PWKTUX2) |\n| 申报单位 | 昆明市人力资源和社会保障局 |\n| 申报单位负责人 | 黄斌(联系电话 63362979) |\n| 运营负责人 | 李云鹏(联系电话 18087147411) |\n| 载体特色 | 云南省首家政府主办、", "昆明市大学生创业园是省级的吗?什么时候评上的?", "这个园区叫什么名字?在昆明哪个位置啊?", "园区是哪年成立的?主管部门是谁?谁在负责运营?", "听说2026年4月园区焕新亮相了,是真的吗?现在归谁管?", "园区的运营公司叫啥?统一社会信用代码是多少?申报单位负责人电话能给一下吗?", "为创业大学生免费提供创业平台的开放式园区 |\n| 主导产业 | 数字经济与电子信息产业 |\n| 载体占地面积 | 3000 平方米 |\n| 孵化场所面积 | 800 平方米 |\n| 载体资产总值 | 400 万元 |\n| 总投资(开园) | 近 100 万元 |\n| 累计投入园区运营资金 | 560 万元 |\n| 可容纳团队(实体)户数 | 50 户(统计表中某口径为 49 个) |\n| 园区等级/称号 | 国家大学科技园体系、省级创业园等(宣传册口径:国家众创空间、", "园区主要支持哪些行业的创业项目?", "整个园区有多大?办公场地和孵化场所各占多少面积?", "园区总共投了多少钱?现在资产值多少?最多能容纳多少个创业团队?", "为什么有的地方写能容纳50户,有的又说49个?哪个数准?", "园区拿过哪些国家级或省级的牌子?比如国家众创空间、科技企业孵化器这些算不算?", "国家级科技企业孵化器等称号口径不一,以正式文件为准) |\n**园区简介(300字内口径)**\n昆明市大学生创业园位于昆明市民航路301号昆明人力资源中心二楼,2009年9月成立,总投资近100万元,是云南省首家免费为创业大学生提供创业平台的开放式园区,隶属于昆明市人力资源和社会保障局,由昆明市大学生创业园服务中心承担日常管理服务。园区总面积约3000平方米,可容纳50个大学生创业团队入驻,为入驻团队提供免费办公场地及配套设施,并配套政策咨询、创业规划、创业指导、创业培训、", "园区简介里说在民航路301号,但地址又写229号,到底哪个对?", "园区给大学生创业提供哪些免费服务?除了场地还有啥?", "园区的服务理念和办园宗旨是怎么说的?", "到2026年5月为止,一共孵化了多少家企业?现在还在孵的有多少?", "园区带动了多少人就业?帮企业拿到了多少创业扶持资金?", "导师对接、跟踪服务、资金扶持、法律援助等全维度创业支持服务。园区坚持“高起点、高标准、高水平”办园宗旨,遵循“优质、高效、便捷”服务原则与“规范、完善、提高”管理理念,致力打造企业家培育摇篮与全省示范创业园区。截至2026年5月,园区累计孵化企业181家,其中在孵31家、成功出园149家,累计带动就业1529人、上缴利税426.2万元,38家企业累计获市级创业扶持资金149.25万元。\n---", "目前园区实际入驻了多少家创业团队?今年新进了几家?", "应届毕业生(含离校2年内)在园区创业的有多少家?今年新增了几家?", "园区里的创业团队一共拿了几个发明专利?今年新申请了多少项?", "入驻团队去年一共赚了多少钱?交了多少税?今年缴了多少?", "园区创业指导专家有多少人?今年组织了多少场创业指导活动?", "> 出处:附件1.2 省级创业孵化载体简介(运营绩效情况)\n| 指标 | 认定以来累计 | 本年度 | 当前实有 |\n|---|---|---|---|\n| 可容纳团队(实体)户数 | — | 50 户 | — |\n| 已入驻团队(实体)户数 | 181 户 | 31 户 | 31 户 |\n| 其中:应届高校毕业生(含离校2年内)参创实体数 | 114 户 | 6 户 | — |\n| 团队(实体)出园数 | 150 户 | 11 户 | — |\n| 返乡农民工参加创办实体数 | 0 户 | 0 户 | — |\n| 入驻团队(实体)发明专利数 | 171 项 | 13 项 | — |\n| 入驻团队(实体)经营收入 | 21911.14 万元 | 161.3 万元 | — |\n| 入驻团队(实体)税收缴纳金额 | 426.2 万元 | 0.22 万元 | — |\n| 入驻团队(实体)获得创业资金扶持金额 | 38 户 | 149.25 万元 | — |\n| 孵化创业实体数 | 181 户 | — | 31 户(在孵) |\n| 带动就业人数 | 1529 人 | 106 人 | — |\n| 运营团队人数 | — | 3 人 | — |\n| 创业指导专家团队人数 | — | 50 人 | — |\n| 开展创业指导服务活动次数 | 83 场 | 18 场 | — |\n| 年末孵化场所利用率 | 90%(0.9) | — | — |\n| 近3年入孵实体孵化成功率 | 95.91% | — | — |\n| 近3年入孵实体到期出园率 | 100%(1) | — | — |\n---", "园区孵化成功率和出园率分别是多少?数据是近3年的吗?", "现在孵化场所的使用率有多高?90%是指什么?", "返乡农民工能在园区创业吗?有没有相关扶持?", "园区累计孵化181家,但表格里‘团队出园数’写150户,差的那31家去哪了?", "运营团队现在有几个人?是不是就3个工作人员管整个园区?", "> 出处:昆明市大学生创业园运行情况统计表(2026年7月).xls —《昆明市创业平台运行情况月统计表》及《年度汇总表》", "昆明市大学生创业园的运行情况统计表是哪份文件?", "2026年7月的昆明创业园数据出自哪个表格?", "《昆明市创业平台运行情况月统计表》和《年度汇总表》是不是同一份材料?", "这份园区数据是来自Excel文件还是其他格式?文件名是什么?", "‘昆明市大学生创业园运行情况统计表(2026年7月).xls’这个文件包含哪些内容?", "### 3.1 2026年7月当月快照\n| 指标 | 数值 |\n|---|---|\n| 园区建筑面积 | 3000 m² |\n| 开园以来累计投入园区运营资金 | 560 万元 |\n| 园区可容纳企业(实体)个数 | 49 个 |\n| 园区实际入驻企业数 | 38 个 |\n| 入驻企业累计带动就业人数 | 213 人 |\n| 入驻企业累计生产经营总额 | 252.15 万元 |\n| 入驻企业累计上缴税利总额 | 1.03 万元 |\n| 开园以来累计成功孵化出园企业(实体)数 | 149 户 |\n| 开展活动场次 | 11 场 |\n| 培训人数 | 362 人 |\n> 备注:报表第3、", "昆明大学生创业园现在有多大?建筑面积是多少?", "园区总共投了多少钱运营?累计花了多少?", "园区最多能容纳多少家企业?现在实际有多少家在园里?", "这些企业一共招了多少人就业?总共赚了多少钱、交了多少税?", "开园到现在一共孵化成功、毕业出园的企业有多少家?今年办了多少场活动、培训了多少人?", "9栏数据为开园以来累计数,第5栏为报送当月实际入驻企业数,其他栏均为当年累计数。", "报表里哪些数字是‘从开园到现在’一直累计的?哪些是‘今年1到7月’的?", "‘实际入驻企业数’是当月的还是全年的?怎么区分累计值和当期值?", "表格第3栏和第9栏为什么标着‘累计’,其他栏又说是‘当年累计’?有啥区别?", "‘报送当月实际入驻企业数’具体指哪一栏?怎么查这个月新进或退出的企业?", "为什么有些指标写‘当年累计’,有些写‘开园以来累计’?怎么看懂这个统计口径?", "### 3.2 2026年1—7月逐月变化(年度汇总表)\n| 月份 | 园区建筑面积(m²) | 累计运营资金(万元) | 可容纳个数 | 实际入驻企业数 | 带动就业(人) | 累计生产经营总额(万元) | 累计上缴税利(万元) | 累计出园(户) | 活动场次 | 培训人数 |\n|---|---|---|---|---|---|---|---|---|---|---|\n| 1月 | 700 | 510 | 18 | 15 | 73 | 28.3 | 0.04 | 139 | 0 | 0 |\n| 2月 | 700 | 510 | 18 | 15 | 73 | 50.4 | 0.08 | 139 | 0 | 0 |\n| 3月 | 700 | 560 | 18 | 5 | 73 | 59.1 | 0.09 | 149 | 0 | 0 |\n| 4月 | 3000 | 560 | 49 | 24 | 175 | 69.73 | 0.10 | 149 | 0 | 0 |\n| 5月 | 3000 | 560 | 49 | 31 | 179 | 161.3 | 0.22 | 149 | 5 | 149 |\n| 6月 | 3000 | 560 | 49 | 31 | 186 | 170.4 | 0.35 | 149 | 7 | 206 |\n| 7月 | 3000 | 560 | 49 | 38 | 213 | 252.15 | 1.03 | 149 | 11 | 362 |\n> 数据说明:4月起园区迁入民航路229号新场地,建筑面积由700㎡增至3000㎡、", "园区4月份搬去民航路229号后,面积、容量和资金都变了,具体怎么变的?", "从1月到7月,入驻企业数、带动就业人数这些数据是怎么一步步增长的?", "3月到4月之间园区发生了什么变化?为什么可容纳数突然从18变成49?", "7月底的数据和年初相比,哪些指标增长最多?比如就业人数、营收、活动场次?", "年度汇总表里每月的数值变化能反映园区发展节奏吗?比如哪个月扩张最快?", "可容纳数由18个增至49个,运营资金由510万元增至560万元。截至7月底实际入驻38个,累计带动就业213人。\n**填报信息**:单位负责人 李云鹏 / 填表人 李禹贤 / 联系电话 18087174891 / 邮箱 714066050@qq.com / 填表时间 2026年7月31日\n---", "园区负责人和填表人是谁?联系方式能发我一下吗?", "想联系园区对接工作,找李云鹏还是李禹贤?电话和邮箱是多少?", "填表时间是2026年7月31日,这个数据是最新的吗?还有效吗?", "单位负责人李云鹏和填表人李禹贤是什么关系?分别负责什么?", "714066050@qq.com这个邮箱能用来咨询入园政策吗?联系电话18087174891是24小时开通的吗?", "> 出处:昆明市大学生创业园入驻企业信息表.xlsx(共39个项目,序号1–39,按“场地安排”列分区分布)\n> 分区口径:加速=孵化加速区、国际=国际创客区、成长=成长区、OPC=OPC创业空间、一楼直播间。", "昆明大学生创业园有哪些分区?", "创业园的场地安排分哪几个区?", "孵化加速区、国际创客区这些分区是怎么划分的?", "OPC创业空间和一楼直播间是不是园区的正式分区?", "园区里说的‘加速’‘国际’‘成长’‘OPC’‘一楼直播间’分别指什么区域?", "### A. 孵化加速区\n(加速1—加速8、加速10,共9家;表中无“加速9”)", "孵化加速区一共有几家入驻企业?", "加速1到加速8,还有加速10,为什么没有加速9?", "孵化加速区总共多少家企业?有没有漏掉编号?", "加速区的企业数量是9家吗?都包括哪些编号?", "创业园孵化加速区包含哪些编号的企业?", "**加速1|云南派音人工智能科技有限公司**\n- 负责人:王思松,2020年毕业于应急管理大学新闻学专业。\n- 简介:OPC一人公司,2026年3月启动,入驻不足一个月即获北京相关投资支持,2026年4月正式注册成立,吸纳就业4人。核心业务主攻人工智能算法及音频模型应用研究,兼备内容创作、运营管理与技术研发综合能力,专注多模态音频技术研发。自研Pine全系列模型家族,覆盖音频识别、向量嵌入、音效生成、配乐创作及数字内容创意中枢,贯通音频理解至声音合成全链路智能。", "云南派音人工智能科技有限公司", "加速1公司叫什么名字?负责人是谁?毕业学校和专业是什么?", "云南派音人工智能科技有限公司的创始人王思松是哪个学校毕业的?", "派音科技什么时候启动和注册的?招了几个人?", "派音科技做的是什么业务?他们的Pine模型系列有什么功能?", "加速1公司主要研究什么技术?音频识别、音效生成这些是不是他们做的?", "**加速2|米勒克尔蓝宝石珠宝产业化(项目)**\n- 负责人:王昱博,2023年3月毕业于美国加州大学圣克鲁兹分校 Multi-/Interdisciplinary 管理学专业。\n- 简介:2026年3月启动,海归留学生发起,覆盖米勒克尔大尺寸蓝宝石新材料与珠宝产业化全链条(晶体生长、材料精密加工、终端珠宝设计)。与产业链上下游深度协同,专注高品质、高附加值蓝宝石珠宝产品,推动蓝宝石新材料与珠宝产业深度融合。", "米勒克尔蓝宝石珠宝产业化(项目)", "加速2项目叫什么?负责人王昱博是哪所学校毕业的?", "米勒克尔蓝宝石珠宝项目是海归做的吗?学的是什么专业?", "这个蓝宝石项目具体做哪些事?晶体生长和珠宝设计都包括吗?", "蓝宝石新材料和珠宝产业怎么融合?跟上下游怎么合作?", "加速2项目主打高附加值蓝宝石珠宝,是真的做实物珠宝还是只做材料?", "**加速3|中泰研学合作(项目)**\n- 负责人:刘泽位,泰国西那瓦大学工商管理博士二年级,昆明市侨联青年委员会副会长,精通英语、熟练泰语。\n- 简介:2026年启动运营,海归在读博士发起,围绕西那瓦大学文理学院管理运营,统筹泰国环球工商职业学院中国区事务。与中泰院校、机构深度合作,提供出国留学、中外合作办学、校企合作、产教融合及企业出海一站式服务。", "中泰研学合作(项目)", "加速3项目名称是什么?负责人刘泽位在读什么博士?", "中泰研学合作项目和泰国西那瓦大学有什么关系?", "这个项目提供出国留学服务吗?还做校企合作和企业出海吗?", "刘泽位会说泰语吗?他在侨联有职务吗?", "中泰研学合作项目覆盖哪些服务?比如中外合作办学、产教融合这些包不包括?", "**加速4|云南宸中低空经济有限公司**\n- 负责人:王爱玲,云南大学EMBA在读,公司负责人。\n- 简介:2026年2月成立,扎根园区的创新型初创企业,专注低空经济全产业链开发运营,依托园区创业扶持体系与营商环境,快速完成项目落地与团队组建,致力于成为云南低空经济领域新兴力量。", "云南宸中低空经济有限公司", "云南宸中低空经济有限公司是干啥的?负责人是谁?什么时候成立的?", "王爱玲是哪家公司的负责人?她现在在读什么学校?这家公司主要做低空经济哪些业务?", "园区里新成立的低空经济公司叫什么名字?成立时间、负责人背景和业务方向能说说吗?", "宸中低空经济是不是2026年2月刚成立的?它在园区享受哪些创业扶持政策?", "云南有没有专注低空经济全产业链的初创企业?叫什么?谁负责?有什么特色?", "**加速5|昆明智海银高文化科技有限公司**\n- 负责人:宋芃燃,昆明文理学院审计学专业在读,中共党员,公司合伙人。\n- 简介:2026年4月成立,聚焦数字化财税与企业咨询服务,覆盖传统财税、AI智能财税、海外财税、财务人才培训及企业综合咨询。依托AIGC技术打造AI集成服务平台,提供财税咨询、代理记账、法律合规、企业管理、出海服务等一站式解决方案。", "昆明智海银高文化科技有限公司", "昆明智海银高文化科技有限公司是做什么的?负责人宋芃燃是哪个学校的?", "这家公司做AI财税服务吗?都提供哪些具体服务,比如代理记账、出海咨询这些有吗?", "智海银高是2026年4月成立的对吧?它用AIGC技术做了个什么平台?", "宋芃燃是中共党员吗?她在昆明文理学院学的是审计学?公司主要服务对象是哪些企业?", "有没有做数字化财税+企业综合咨询的公司?叫什么?覆盖海外财税和财务培训吗?", "**加速6|人工智能机器人大模型训练(项目)**\n- 负责人:周凯,2022年毕业于天津大学机械设计及其自动化专业。\n- 简介:2026年4月成立,聚焦人工智能机器人大模型训练,围绕大模型研发与落地提供全流程数据支撑与技术服务,开展数据采集、清洗、标注、模型训练及优化迭代,推动机器人大模型在多场景落地应用。", "人工智能机器人大模型训练(项目)", "人工智能机器人大模型训练项目是哪个团队做的?负责人周凯什么学历背景?", "这个大模型训练项目是2026年4月启动的吗?具体做数据采集、标注还是模型优化?", "周凯是天津大学毕业的?他带队做的机器人项目主要解决哪些落地场景问题?", "园区里有没有专门做机器人大模型‘训练-优化-应用’全流程的技术项目?叫什么?", "大模型训练项目包括数据清洗和迭代优化吗?是否面向多行业场景落地?负责人是谁?", "**加速7|云南廷秀文旅康养服务(项目)**\n- 负责人:李雨桐,2025年毕业于日本樱美林大学老年社会学硕士。\n- 简介:负责人计划5月归国创业。立足云南“旅居云南”趋势与银发经济增长需求,解决民宿、旅居空间、康养点位在服务标准、适老能力、长期居住支持和运营转化方面的短板,建立面向银发人群的文旅旅居服务支持体系。", "云南廷秀文旅康养服务(项目)", "云南廷秀文旅康养服务项目什么时候开始运营?负责人李雨桐是哪国留学回来的?", "李雨桐在日本读的老年社会学硕士?她回国后要做的康养项目主要解决民宿和旅居点哪些问题?", "廷秀项目针对银发人群,具体怎么提升服务标准、适老能力和运营转化率?", "云南现在推‘旅居云南’,有没有专门做老年文旅旅居服务支持体系的创业项目?", "这个康养服务项目5月启动吗?聚焦旅居空间、民宿和康养点位的服务短板,对吗?", "**加速8|瀚颖AI+教育信息咨询(项目)**\n- 负责人:王翰啸,2019年毕业于云南大学滇池学院财务管理专业。\n- 简介:昆明瀚颖“AI+数字教育”项目2026年4月成立,由创业培训服务领域人员发起,融合AI与教育教学体系,打造集课程研发、人才培育、企业赋能、师资共建于一体的全链条数字化教育服务生态。", "瀚颖AI+教育信息咨询(项目)", "瀚颖AI+教育信息咨询项目是哪家机构发起的?负责人王翰啸是哪个学校毕业的?", "这个项目是不是2026年4月成立的?它搞AI+教育,具体做课程研发、师资共建还是企业赋能?", "王翰啸是云南大学滇池学院毕业的?他们打造的数字教育生态包含哪些环节?", "有没有融合AI技术和教育教学体系的教育服务项目?在昆明园区叫什么名字?", "瀚颖的AI+教育项目提供人才培育和企业赋能服务吗?属于全链条数字化教育生态吗?", "**加速10|云南大学AI+联合创业服务平台(项目)**\n- 负责人:顾耘华,云南大学艺术与设计学院艺术设计在读硕士。\n- 简介:云南大学在校研究生、毕业生和创业导师共同发起的AI创业平台。以OPC社区AI人才培养与就业服务为核心,覆盖AI短视频、AI漫剧、AI+PPT制作美化等。已帮助多名大学生成功开展OPC创业;联合云南AIGC产业联盟打造“育笋计划”公益品牌,辐射大学生就创业500人+;王升雅《生态秘境》插画在《人与自然》杂志连载六期;王洋《智绘古海——AI复原寒武纪古生物》获国家级创赛金奖;杨沐晨打造西南城市鸟类多样性生态视觉品牌,承办7项生态展览;王润曦协同云南省委宣传部助力会泽白雾村打造乡村文旅振兴名片。", "云南大学AI+联合创业服务平台(项目)", "云南大学AI+联合创业服务平台是干什么的?", "顾耘华老师负责的那个AI创业平台主要做哪些AI方向?比如AI短视频、AI漫剧这些有具体案例吗?", "云南大学的AI创业平台帮大学生创业成功过吗?有没有拿国家级奖项的项目?", "育笋计划是什么?是不是云南AIGC产业联盟搞的?覆盖了多少大学生?", "王升雅、王洋、杨沐晨、王润曦他们各自做了什么AI相关项目?分别有什么成果?", "### B. 国际创客区\n(国际1—国际7,共7家)", "国际创客区一共有几家?都叫什么名字?", "园区里的国际1到国际7是哪几个国家的团队在做?", "国际创客区的位置在哪?里面都是外国留学生开的公司吗?", "国际创客区总共多少个空间?每个空间对应一个项目吗?", "国际1到国际7是按什么顺序编号的?有没有空着没入驻的?", "**国际1|仰光客厅(项目)**\n- 负责人:缅甸籍留学生,毕业于云南大学经济学院。\n- 简介:缅甸留华学生发起的中缅文旅融合项目,以仰光近郊沉浸式文化营地为核心载体,面向中国客群、在缅中资机构及本地青年,集住宿、特色餐饮、文化体验、城市导览于一体,与本地社区、手工艺人合作带动就业与增收,搭建跨文化交流平台。", "仰光客厅(项目)", "仰光客厅是谁创办的?是不是缅甸留学生做的?", "仰光客厅这个项目主要是面向谁的?中国游客、在缅中资企业还是缅甸本地年轻人?", "仰光客厅除了住宿和吃饭,还能体验什么缅甸文化?跟当地手艺人怎么合作的?", "仰光客厅是不是在缅甸仰光郊区搞了个沉浸式营地?具体做什么活动?", "仰光客厅算不算中缅文化交流平台?有没有实际带动当地人就业增收?", "**国际2|千楣之路(项目)**\n- 负责人:缅甸籍留学生,现就读于云南大学工商管理学院。\n- 简介:缅甸籍留学生创立,主营缅甸传承两千余年的传统护肤产品檀娜卡(黄香楝树干制成,缅甸已于2025年申报联合国非遗)。打造传统原木套装、便捷粉泥、现代美妆三大产品线,以“文化内容+跨境电商”模式引入中国市场。", "千楣之路(项目)", "千楣之路卖的是不是缅甸传统护肤品檀娜卡?", "檀娜卡是用什么做的?听说缅甸2025年要申非遗,是真的吗?", "千楣之路的产品分哪几类?原木套装、粉泥和现代美妆线都有吗?", "千楣之路怎么把缅甸檀娜卡卖到中国?靠小红书还是跨境电商?", "千楣之路是哪个学校的缅甸留学生做的?是不是云大工商管理学院的?", "**国际3|联翼航空技术低空经济应用国际化拓展(项目)**\n- 负责人:杨俊蓝,2023年毕业于云南财经大学电子商务专业,瑞士商学院MSC硕士在读。\n- 简介:2026年4月启动,在读硕士发起,立足云南面向东南亚的区位门户优势,开展低空经济业务国际化拓展,在航空通航、人工智能与数字信息、教育培训方向布局,输出无人机应用、低空数据服务、跨境技能培训等内容,推动低空经济产业、人才、服务双向辐射东南亚。", "联翼航空技术低空经济应用国际化拓展(项目)", "联翼航空技术这个项目是做什么低空经济业务的?", "杨俊蓝是哪个学校毕业的?现在在哪儿读研?项目什么时候启动的?", "联翼航空技术主要面向东南亚哪些国家拓展?做无人机应用还是数据服务?", "低空经济培训是教外国人飞无人机吗?还是也教国内学员?", "联翼航空技术怎么利用云南的区位优势做国际化?有没有具体输出到东南亚的案例?", "**国际4|酷享野农AI农业(项目)**\n- 负责人:晏和波,西南林业大学农业工程与信息技术专业在校研究生。\n- 简介:2026年4月启动,西南林业大学在校研究生发起,聚焦智慧农业、科研种植、农旅融合,用AI智能视觉提供农业宣传内容制作服务。与科研机构合作开展高原早熟苹果科研种植,与地方政府共建共享农庄,助力乡村振兴。", "酷享野农AI农业(项目)", "酷享野农AI农业这个项目是哪个学校的学生搞的?负责人叫啥名字?", "晏和波是谁?他在西南林业大学读什么专业?", "酷享野农AI农业项目什么时候开始的?主要干些啥?比如用AI做农业宣传吗?", "这个农业项目是不是在种高原早熟苹果?跟科研机构和地方政府怎么合作的?", "智慧农业、农旅融合、科研种植这些词具体指啥?酷享野农项目怎么把它们结合起来?", "**国际5|滇缅国际设计(项目)**\n- 负责人:张浩鹏,2022年毕业于云南财经职业学院审计学专业。\n- 简介:2026年4月启动,依托一带一路机遇,立足滇缅对外开放节点开展工程设计服务,创新跨国协同设计模式,融入绿色建造技术,覆盖交通、城乡基建等领域,与境外机构合作推动跨境项目落地。", "滇缅国际设计(项目)", "滇缅国际设计是做什么的?是不是专门帮云南和缅甸那边做工程设计?", "张浩鹏是哪个学校毕业的?学的什么专业?", "这个设计公司啥时候成立的?一带一路跟他们有啥关系?", "他们说的跨国协同设计模式是啥意思?绿色建造技术用在哪些项目上?", "交通和城乡基建类的设计,他们有没有跟国外机构一起落地过实际项目?", "**国际6|昆明舒诺生物科技有限责任公司**\n- 负责人:舒岐涛,云南林业职业技术学院环境艺术专业在校生。\n- 简介:2026年4月成立,聚焦生物科技与宠物营养领域,围绕精准营养、全生命周期养护、清洁标签开展业务,以科学配方与生物活性技术打造宠物精细化营养产品。", "昆明舒诺生物科技有限责任公司", "昆明舒诺生物科技是干啥的?是不是做宠物食品或者营养品的?", "舒岐涛是哪个学校的学生?学的什么专业?", "这家公司什么时候成立的?主打‘精准营养’‘全生命周期养护’具体指什么?", "清洁标签、生物活性技术这些词听起来挺专业的,他们产品到底有啥特别?", "舒诺生物的产品是不是针对猫狗不同年龄阶段做的?配方科学在哪?", "**国际7|达岸教育管理(云南)有限公司**\n- 负责人:吕琳,2022年毕业于泰国暹罗大学MBA研究生专业。\n- 简介:专注德国双元制教育与德国留学一站式服务(2024年启动),与德国院校合作,提供双元制培养、公立院校申请、德语培训、签证办理、境外陪护等全流程服务,帮助学生实现带薪留学、学历提升与稳定就业。", "达岸教育管理(云南)有限公司", "达岸教育是做德国留学服务的吗?能办双元制吗?", "吕琳是在泰国读的MBA?她现在负责达岸教育的哪些业务?", "德国双元制留学包括德语培训、签证、境外陪护这些服务吗?", "达岸教育从哪年开始做这事儿的?跟哪些德国学校有合作?", "通过他们去德国留学能不能边学边赚工资?最后能拿到学历和工作机会吗?", "### C. 成长区\n(成长1—成长8,共8家)", "成长区一共有几家创业公司?编号是从成长1到成长8吗?", "成长区和其他区域(比如国际4、5)有啥区别?是按发展阶段分的吗?", "成长1到成长8这些项目都在同一个地方办公吗?属于园区什么类型的企业?", "成长区的企业是不是都处于初创或快速成长阶段?有没有统一的支持政策?", "我想了解成长区某一家公司,比如成长3,该怎么查它的详细信息?", "**成长1|花仙子园艺肥料(云南)有限公司**\n- 负责人:翁淞耀,2023年毕业于云南师范大学社会体育指导与管理专业。\n- 简介:2025年4月成立,专注家庭园艺肥研发生产,针对产品异味大、品质不稳、使用不便等痛点推出安全无异味、易操作系列产品,已实现规模化生产。采用线上线下融合渠道,服务家庭养花、阳台种植民生需求。", "花仙子园艺肥料(云南)有限公司", "花仙子园艺肥料公司是哪个学校毕业的人创办的?创始人叫什么名字?", "这家做家庭园艺肥的公司是哪年成立的?主要解决养花人哪些实际问题?", "花仙子肥料有没有异味?产品是不是容易操作?现在是批量生产了吗?", "翁淞耀是云南师大哪个专业的?他做的肥料线上线下都怎么卖?", "花仙子公司服务对象主要是谁?比如阳台种菜、家里养花的人能用吗?", "**成长2|研X——同行者网络(项目)**\n- 负责人:陈品祥,2024年昆明理工大学硕士毕业,兼职任教昆明冶金职业大学。\n- 简介:面向全国1621万硕博及6000万泛科研人群的一站式学术成长与资源共享平台。首创“1+6+N”生态(1个实名制平台+文献工具导航、代码共享交易、就业创业对接、学术/情感社交婚恋、科教娱乐、研究生严选商城6大模块)。已完成安卓、iOS、Windows三端开发并上架,已有全国300余所高校注册,两位高校教授以个人资金投入种子轮。", "研X——同行者网络(项目)", "研X平台是给哪些人用的?说的‘1621万硕博’和‘6000万泛科研人群’具体指什么?", "同行者网络这个APP有苹果和安卓版吗?高校老师有没有投资?投了多少?", "‘1+6+N’生态里的6大模块都包括啥?比如代码共享、找工作、谈恋爱这些真都有?", "研X平台现在有多少学校在用?是学生自己注册还是学校统一组织?", "陈品祥是哪个学校毕业的?现在还在教书吗?在哪儿任教?", "**成长3|朵哈·玫瑰特色产业链(项目)**\n- 负责人:刘志贤,云南轻纺职业学院艺术设计专业大二在校生。\n- 简介:2026年4月启动,云南轻纺学院在校大学生发起,立足昆明安宁八街核心产区,以地标产品重瓣红玫瑰为核心原料,主打玫瑰米酿特色饮品,与省内农业产业化龙头企业合作实现原料优选、标准化生产与品质管控。", "朵哈·玫瑰特色产业链(项目)", "朵哈玫瑰项目是谁发起的?大学生创业能做玫瑰米酿吗?", "八街的重瓣红玫瑰是地理标志产品吗?他们跟农业龙头企业怎么合作的?", "玫瑰米酿是饮料还是酒?生产是标准化的吗?品质怎么保证?", "刘志贤是哪个学校哪个年级的学生?项目是2026年4月才开始的吗?", "朵哈项目只做玫瑰米酿,还是也会开发其他玫瑰相关产品?", "**成长4|昆明舒护安养老服务有限责任公司**\n- 负责人:严宇佳,2019年毕业于云南艺术学院广播电视编导专业。\n- 简介:OPC一人公司,入驻园区孵化仅一个月成功吸纳就业3人。专注正规医疗护理服务,主营医院专属陪护与居家上门护理,覆盖术后康复、失能半失能照料、慢病护理、偏瘫照护、生活监护等全场景,与医疗机构深度合作。", "昆明舒护安养老服务有限责任公司", "舒护安养老公司是做什么护理服务的?比如陪病人去医院或者上门照看老人行不行?", "严宇佳是学广播电视编导的,怎么转行做医疗护理?公司是个人开的吗?", "这家公司才孵化一个月就招了3个人?主要服务哪些病人群体?比如术后、偏瘫、慢病老人?", "舒护安跟医院有合作吗?是直接对接医院安排陪护,还是自己接单?", "居家上门护理包括生活监护吗?比如帮独居老人买菜、提醒吃药这些算不算?", "**成长5|南菌优培——食用菌专用生物制剂创新与高原特色野生菌高值化全链开发(项目)**\n- 负责人:王荣飞,2026年毕业于中山大学外科学专业。\n- 简介:三年以上研发基础与产业化探索,核心团队含分析化学博士、农业硕士、医学硕士等。计划2026年7月完成公司注册并启动。聚焦食用菌专用生物制剂(引入海藻糖-6-磷酸、寡糖等生物源成分)与高原特色野生菌(硬皮地星、硬皮马勃)高值化全链开发。", "南菌优培——食用菌专用生物制剂创新与高原特色野生菌高值化全链开发(项目)", "南菌优培项目什么时候正式注册公司?负责人王荣飞是学医的,怎么搞食用菌的?", "他们研发的生物制剂加了海藻糖-6-磷酸和寡糖?这些成分是天然的吗?", "硬皮地星和硬皮马勃是云南特有的野生菌吗?高值化全链开发具体指哪几个环节?", "团队里有分析化学博士和农业硕士,这个项目已经有三年研发基础了?", "南菌优培是专注做生物制剂,还是也做野生菌的种植、加工、销售整条链?", "**成长6|五华区丽裳文化艺术工作室**\n- 负责人:王娟,2024年毕业于昆明幼儿师范高等专科学校幼儿教育专业。\n- 简介:2026年4月成立,专注传统文化、民俗活动、茶文化及国风品牌活动策划执行与文旅研学。依托云南自贸区优势,推动民俗文创、茶产品、干花制品及高原特色产品出海,搭建“账号引流+内容传播+产品出海”运营体系。", "五华区丽裳文化艺术工作室", "丽裳文化艺术工作室是干啥的?主要做传统文化和茶文化活动策划吗?", "王娟老师开的丽裳工作室什么时候成立的?是不是专门帮云南特产比如茶和干花出海?", "五华区丽裳文化艺术工作室的负责人是谁?她学的是幼教专业,怎么转行做国风文旅策划了?", "丽裳工作室说搞‘账号引流+内容传播+产品出海’,具体是怎么操作的?有做抖音或小红书推广吗?", "这个工作室是不是靠云南自贸区政策优势,帮民俗文创和高原特色产品卖到国外?", "**成长7|云南星瑞航空(项目)**\n- 负责人:尹永浩,2019年毕业于滇西科技师范学院亚洲微电影学院数字媒体技术专业。\n- 简介:2026年4月启动,专注低空智能应用,以大疆行业级无人机为核心装备,业务涵盖无人机测绘、三维建模、数字孪生、应急救援、公共安全巡检、夜间红外搜救及电力、光伏、油气管道、森林防火等作业,并深耕文物数字化保护。具备硬件销售、数据处理、技术支持、人才培训一体化能力。", "云南星瑞航空(项目)", "星瑞航空这个项目是真搞飞机还是只用无人机?他们用的是大疆哪款行业机?", "尹永浩团队能做文物数字化保护吗?除了拍照片建模,还做三维重建和数字孪生吗?", "云南星瑞航空接不接电力巡检、光伏板检测或者森林防火巡查这类活儿?夜间红外搜救也行吗?", "他们除了飞无人机,还卖设备、处理数据、培训飞手吗?是一条龙服务吗?", "星瑞航空是2026年4月刚启动的?负责人尹永浩是学数字媒体技术的,怎么切入低空智能应用领域的?", "**成长8|综合性云南直播私域服务平台**\n- 负责人:金满满,2026年7月毕业于周口师范学院学前教育专业。\n- 简介:综合性云南直播私域服务平台,深耕云南本土特色资源,打造自主IP形象,以短视频创作+全域直播运营为核心,搭建全国性自主私域流量池;同时搭建大学生创业孵化通道,构建人才迭代体系。", "综合性云南直播私域服务平台", "那个云南直播私域服务平台叫啥名?是不是专门帮本地商家做抖音直播和私域引流的?", "金满满刚毕业就创业做直播平台?她学的是学前教育,怎么转行搞短视频创作和全域运营?", "这个平台说打造自主IP形象,还建全国性私域流量池,具体指微信社群、小程序还是APP?", "他们真在带大学生创业孵化?有没有实习、培训或者人才推荐机制?", "综合性云南直播私域服务平台是不是主要挖云南特产、非遗、旅游这些本土资源来拍短视频和开直播?", "### D. OPC创业空间\n(场地安排=OPC,共6家)", "OPC创业空间在哪?里面总共安排了几家单位?都叫D开头的吗?", "D. OPC创业空间是不是就是指那6家入驻企业共用的办公场地?名字里的OPC是缩写吗?", "OPC创业空间里都有哪些项目?是不是包括中外研学、直播平台这些?", "想租OPC的空间办公,得找谁申请?有没有工位或独立办公室可选?", "OPC创业空间和园区其他孵化场地有啥不一样?是不是专供成长系列项目用的?", "**D1|中外青少年研学(项目)**(OPC)\n- 负责人:王得荣,2022年武汉大学法学专业毕业,2024—2025年英国剑桥大学短期交流学习。\n- 简介:国际游学与教育交流服务项目,已与美国、英国、澳大利亚、新加坡等多国顶尖教育机构达成长期直营战略合作,为青少年、在职教师及家长提供一站式全球化教育交流解决方案。", "中外青少年研学(项目)", "中外青少年研学项目跟哪些国家合作?英美澳新加这些是不是都有直签学校?", "王得荣老师有剑桥交流经历,这个研学项目是不是主打高端国际游学?教师和家长也能报名吗?", "这个研学服务是一站式搞定签证、课程、住宿和保险,还是只负责对接海外机构?", "他们跟国外教育机构是直营合作,不是通过中介?有没有合作院校名单可以看?", "D1项目除了青少年游学,还做在职教师海外研修或亲子家庭出境教育旅行吗?", "**D2|星杭商务信息咨询昆明有限责任公司**\n- 负责人:杨之华,2024年6月毕业于云南师范大学教育学专业。\n- 简介:主要经营商务服务、信息咨询培训、商务接待及服务保障配套。", "星杭商务信息咨询昆明有限责任公司", "星杭商务信息咨询昆明有限责任公司是干啥的?负责人是谁?学什么专业的?", "D2那家公司主要做哪些业务?杨之华是负责人吗?她是不是云南师范大学毕业的?", "杨之华创办的星杭商务咨询公司提供培训和商务接待服务吗?她什么时候毕业的?", "星杭商务这家公司是教育学背景的人开的吗?都做哪些商务服务和信息咨询?", "D2园区企业‘星杭商务’的负责人学历和主营业务分别是什么?", "**D3|蓝智科技(项目)**\n- 负责人:徐安厅,2026年毕业于昆明城市学院计算机科学与技术专业。\n- 简介:OPC一人公司,2026年5月启动,从业3人。立足西南蓝莓特色种植产业,专注农业物联网技术研发与智慧基地数字化运营,自研蓝智全系列智慧种植监控系统,涵盖田间环境监测、基质养分检测、水肥灌溉、种植生产数据管理等。", "蓝智科技(项目)", "蓝智科技是做什么的?徐安厅是负责人吗?他哪年毕业?", "D3蓝智科技项目是不是搞农业物联网的?专门服务蓝莓种植?有自研监控系统吗?", "蓝智科技的智慧种植系统能监测田间环境和水肥灌溉吗?团队几个人?什么时候启动的?", "徐安厅带的蓝智科技是不是西南地区做蓝莓数字化种植的?他们自己开发了全系列监控系统?", "蓝智科技这个OPC公司是2026年5月开始做的吗?专注农业物联网和智慧基地运营?", "**D4|云品出滇·纸享万家(项目)**\n- 负责人:王帅,2024年毕业于昆明理工大学电子信息工程专业。\n- 简介:2026年3月成立,以新型悬挂式抽纸为核心切入产品,依托AI工具与电商平台,打造国内电商+跨境电商双增长模式的日用纸品品牌。当前已在拼多多平台实现真实销售验证,未来向东南亚跨境电商扩展。", "云品出滇·纸享万家(项目)", "云品出滇·纸享万家是卖抽纸的吗?王帅是负责人?哪个学校毕业的?", "D4这个项目是不是用AI工具做电商抽纸品牌?现在在拼多多卖得怎么样?", "‘纸享万家’是不是主打悬挂式抽纸?未来要往东南亚做跨境电商?", "王帅的云品出滇项目是2026年3月成立的吗?有没有真实销售验证?", "云品出滇这个日用纸品品牌靠AI和电商平台双轮驱动?核心产品是新型悬挂抽纸?", "**D5|云迹轻旅——昆明本土一站式轻量化文旅服务工作室(项目)**\n- 负责人:王海璇,云南大学工商管理(MBA)毕业,10年云南文旅从业经验。\n- 简介:单人OPC轻资产运营,构建“内容+服务+技术”三位一体业务矩阵:文旅自媒体IP孵化、私人小众旅行定制、非遗自然研学小团、文旅商业咨询策划、AI数字文旅技术服务五大板块。面向C端游客与B端本地文旅商家提供服务。", "云迹轻旅——昆明本土一站式轻量化文旅服务工作室(项目)", "云迹轻旅是做什么文旅服务的?王海璇有十年经验吗?是不是云南大学MBA毕业的?", "D5云迹轻旅工作室提供私人定制旅行和非遗研学小团吗?也帮本地商家做文旅策划?", "云迹轻旅是不是‘内容+服务+技术’三块都做?包括自媒体IP孵化和AI数字文旅服务?", "王海璇的轻旅工作室是单人运营的吗?面向游客和本地文旅商家都服务?", "云迹轻旅有没有做AI相关的文旅技术服务?还做商业咨询和小众旅行定制?", "**D6|启元人工智能科技(项目)**\n- 负责人:李瑞康,2017年毕业于吉林动画学院广播电视编导专业。\n- 简介:OPC一人公司,专注8-12岁儿童人工智能工具的技术推广与科普体验服务,自主研发适配低龄儿童的AI实操工具包,依托共享空间开展少儿AI创意科技体验沙龙(AI绘画、AI音乐创作、AI视频制作)。", "启元人工智能科技(项目)", "启元人工智能科技是教小孩用AI的吗?李瑞康是负责人?学什么专业的?", "D6启元科技是不是专做8到12岁儿童AI科普的?有自己研发的工具包吗?", "启元人工智能科技在共享空间办AI绘画、AI音乐这些少儿体验沙龙吗?", "李瑞康的公司是不是一人OPC?专注儿童AI工具推广和实操体验?", "启元科技的AI实操工具包是为低龄儿童设计的吗?开展过AI视频制作这类活动?", "> 其他在简介中表述为“OPC一人公司”但按场地安排归入其他分区的项目:云南派音人工智能科技(加速1)、昆明舒护安养老服务(成长4)、云南大学AI+联合创业服务平台(加速10)等。\n> 另:云南大学AI+联合创业服务平台完整介绍见【孵化加速区-加速10】。", "云南派音人工智能科技、昆明舒护安养老服务、云南大学AI+联合创业服务平台这几家为什么在简介里写的是OPC一人公司,但场地安排却不在OPC分区?", "云南大学AI+联合创业服务平台属于哪个分区?它的详细介绍在哪能查到?", "简介里说的OPC一人公司项目有哪些被分到其他分区了?具体是哪几家?", "云南大学AI+联合创业服务平台是不是在加速10区?它和OPC一人公司的关系是什么?", "‘OPC一人公司’这个说法在简介里出现,但实际场地没分在OPC区,那这些项目到底算不算OPC?比如云南派音、舒护安这些?", "### E. 一楼直播间\n(共1家)", "一楼直播间总共安排了几家企业?只有1家吗?", "园区一楼的直播间是不是只有一家公司用?是哪家?", "一楼那个直播间是给谁用的?整个一层就一个直播间对吧?", "园区里标着‘E. 一楼直播间’的地方,到底入驻了几家团队?", "一楼直播间这个分区是不是只有一个名额?有没有可能后续加企业?", "**一楼直播间|昆明屿澈电子商务有限公司**\n- 负责人:杨君武,2024年6月毕业于云南交通职业技术学院建设工程管理专业。\n- 简介:高校毕业生发起的东南亚跨境电商AI智能直播+短视频出海项目,依托TikTok Shop正规跨境渠道,面向马来西亚、泰国、越南、菲律宾四国,主营日用百货、家居用品、电子配件、文创收纳、便携小家电等。采用AI数字人7×24小时无人直播、AI短视频批量引流、1688一件代发轻资产模式。", "昆明屿澈电子商务有限公司", "昆明屿澈电子商务有限公司是做什么的?他们直播卖什么产品?", "昆明屿澈这家公司是不是毕业生创业的?负责人是谁、什么学校毕业的?", "昆明屿澈做的是TikTok Shop跨境直播,主要面向哪些国家?卖什么类型的商品?", "昆明屿澈用AI数字人做24小时直播,还用1688一件代发,这种模式具体是怎么运作的?", "杨君武创办的昆明屿澈,是不是高校毕业生做的东南亚跨境电商项目?用的是AI短视频引流吗?", "### F. 信息表中“场地安排”列为空的其他项目\n《入驻企业信息表》中以下项目(共8家)的“场地安排”列未标注分区,照原文列出供全量归档:\n**云南上古绝学文化发展有限公司(简称:上古书院)**", "信息表里‘场地安排’那一栏为空的企业有哪些?一共几家?", "《入驻企业信息表》中没写分区的公司都有谁?比如上古书院是不是其中之一?", "上古书院(云南上古绝学文化发展有限公司)为什么在信息表里没填场地安排?它分到哪个区了?", "园区里有8家企业在信息表里‘场地安排’列是空的,除了上古书院还有哪些?", "‘场地安排’列为空的项目是不是都没分配物理空间?还是只是表格漏填了?上古书院现在实际在哪办公?", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:赵泳淇,2024年毕业于贵州大学教育技术学专业。", "入驻企业完整名录一共有多少家?是39家吗?", "39家入驻企业名单在哪能看到?是不是叫‘入驻企业完整名录’?", "赵泳淇负责的是哪家企业?她是不是39家中某一家的负责人?", "赵泳淇是贵州大学毕业的教育技术学专业学生,她创办或负责的企业在39家名录里吗?", "39家企业的负责人信息都公开吗?比如赵泳淇的毕业院校和专业这些细节是从哪来的?", "4. 入驻企业完整名录(39家/信息表)\n- 简介:专注上古家学文化传承与国学赋能的文化机构,以“修身齐家”为核心理念,业务涵盖家学文化课程体系研发、国学经典文化交流、家训家规个性化定制及文化研学,走“从文化体验到家族传承”路径。", "园区里有哪些做国学和家文化传承的公司?", "有没有专门教家训家规或者搞家族文化研学的机构?", "哪家企业是以‘修身齐家’为理念、做国学课程和文化体验的?", "园区里做上古家学文化、能帮定制家训家规的公司叫什么?", "我想找一家把文化体验和家族传承结合起来的国学机构,园区有吗?", "**中越生物医疗(项目)**", "中越生物医疗(项目)", "中越生物医疗这个项目具体是做什么的?", "园区里有个叫‘中越生物医疗’的项目,它主要干啥?", "中越生物医疗是企业还是合作项目?属于哪个领域?", "听说园区有中越生物医疗项目,它跟生物医药有关吗?", "中越生物医疗项目是哪个单位在运营?涉及哪些技术方向?", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:武妙英(VŨ DIỆU ANH),越南籍留学生,云南大学工商管理专业在读,精通越语、法语、中文、英语。", "园区入驻企业里有越南籍负责人吗?叫什么名字?", "武妙英是谁?她在园区哪个公司任职?", "云南大学越南留学生武妙英,在园区负责哪家企业?", "会四门语言(中英越法)的越南籍负责人在园区哪家企业?", "谁是园区里那个越南籍、在云大读工商管理、懂四国语言的企业负责人?", "4. 入驻企业完整名录(39家/信息表)\n- 简介:聚焦越南标准化生物蛋白生产联合实验室建设,计划覆盖区域80%以上缺口市场,依托中越技术协同与资源互补开展高纯度生物活性蛋白合规化跨境生产与出口,满足疫苗、诊断试剂及医疗器械领域需求。已获越南五部门联合认证。", "中越生物医疗项目做的蛋白生产有什么特别之处?", "这个中越联合实验室生产的生物蛋白用在哪些地方?", "中越生物医疗项目已经拿到越南哪些认证了?", "他们说覆盖越南80%以上市场缺口,具体是哪类蛋白产品?", "中越生物医疗的蛋白生产怎么做到合规跨境出口的?满足疫苗和试剂需求吗?", "**中越文旅交流(项目)**", "中越文旅交流(项目)", "园区有没有做中越之间旅游或文化交流的项目?", "中越文旅交流项目具体开展哪些活动?", "想了解中越两国在文化旅游方面的合作,园区有相关项目吗?", "中越文旅交流是企业还是政府合作项目?主要面向什么人群?", "园区里叫‘中越文旅交流’的项目,和旅游、非遗、语言学习有关吗?", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:阮胡竹珠,云南民族大学广播电视专业在读(2027年毕业)。", "阮胡竹珠是谁?她在园区哪个企业工作?她是云南民族大学哪个专业的学生?她什么时候毕业?", "园区里叫阮胡竹珠的负责人是哪一家企业的?她的学校和专业是什么?", "入驻企业名单里有位叫阮胡竹珠的同学,能查到她的详细信息吗?比如学校、专业、毕业时间?", "云南民族大学广播电视专业的在读生阮胡竹珠,在园区负责哪家企业?", "阮胡竹珠是2027届毕业生?她在园区具体做什么?", "4. 入驻企业完整名录(39家/信息表)\n- 简介:搭建中越文化旅游交流平台,提供旅游咨询、行程定制、文化体验、语言服务及跨境旅游推广,依托广播电视专业背景与中越双语优势开展中越旅游宣传内容制作、新媒体运营、跨境文化交流活动策划。", "园区里有没有做中越旅游交流的公司?主要提供哪些服务?", "哪家企业专门搞中越文化旅游合作?比如旅游定制、语言服务这些?", "做中越旅游宣传和新媒体运营的是哪家入驻企业?有双语能力吗?", "有没有企业既懂越南语又会做旅游短视频和活动策划的?", "搭建中越文旅平台的企业,具体能帮游客做什么?比如订行程、体验文化这些?", "**“绿态”环保科技项目**", "“绿态”环保科技项目", "‘绿态’环保科技项目是哪家企业?它主要做什么业务?", "园区里叫‘绿态’的项目是干什么的?属于哪个团队?", "‘绿态’这个名称对应的是哪个入驻企业?有什么特色?", "有没有叫‘绿态’的环保类创业项目在园区?", "‘绿态’环保科技项目是独立企业还是某个团队的项目名称?", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:陈国旗,云南民族大学在读留学生,负责海外工厂对接、中越跨境供应链统筹及中国全渠道分销网络核心运营。", "陈国旗是哪家企业负责人?他是留学生吗?负责什么工作?", "园区入驻企业里有位叫陈国旗的越南留学生,他在做什么业务?", "负责中越供应链和中国分销的那位越南留学生是谁?属于哪家公司?", "陈国旗在园区管海外工厂对接和国内销售渠道,他具体负责哪家企业?", "哪位入驻企业负责人同时管越南工厂和中国全渠道销售?是不是叫陈国旗?", "4. 入驻企业完整名录(39家/信息表)\n- 简介:紧抓“禁塑令”及绿色环保政策导向,专注天然槟榔叶(mo cau)环保餐具研发、跨境贸易与新媒体全渠道销售。在越南本土自有实体生产工厂,100%越南本土完成从源头集采到杀菌包装,成品经正规清关出口至中国,在昆明园区设“中国市场总分销库与营销/直播工作室”,构建“抖音直播+货架电商双驱闭环”。", "‘绿态’项目做的环保餐具原料是什么?是不是用槟榔叶做的?", "天然槟榔叶餐具是哪家企业研发的?在越南生产、中国销售吗?", "禁塑令背景下做环保餐具的企业有哪些?怎么卖到中国的?有抖音直播吗?", "‘绿态’的餐具在越南哪里生产?怎么运到昆明?在园区有仓库和直播间吗?", "用mo cau(槟榔叶)做的可降解餐具,是哪个园区企业做的?清关和销售模式是怎样的?", "**昆明滇油石化有限责任公司**", "昆明滇油石化有限责任公司", "昆明滇油石化有限责任公司是哪家企业?", "滇油石化在昆明吗?", "昆明滇油石化有限责任公司全称是什么?", "滇油石化这家公司名字怎么写?", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:李政德,预计2027年毕业于云南国土资源职业学院地球物理勘探专业。", "入驻园区的企业一共有多少家?", "园区入驻企业名单总共有39家对吗?", "李政德是谁?他在园区负责什么?", "李政德是云南国土资源职业学院哪个专业的?他什么时候毕业?", "地球物理勘探专业是哪个学校开的?李政德读的是本科还是专科?", "4. 入驻企业完整名录(39家/信息表)\n- 简介:专注生物基纳米驱油剂的研发、生产与销售,为国内低渗透油藏及老油田二次/三次采油提供绿色高效增产解决方案。目标用户:中石油(华北、长庆、新疆、大庆等)、中石化(胜利、中原等)、中海油(渤海等)下属老油田;延伸至一带一路沿线国家老油田(中亚、中东、非洲)及民营油服企业(如延长石油)。", "生物基纳米驱油剂是干什么用的?", "这家企业主要做低渗透油藏的什么产品?", "他们给中石油、中石化哪些油田提供服务?", "老油田二次采油和三次采油有什么区别?他们方案绿色高效体现在哪?", "一带一路沿线哪些国家的老油田会用他们的产品?", "**Facebook越南市场普洱茶与建水紫陶壶跨境电商(项目)**", "Facebook越南市场普洱茶与建水紫陶壶跨境电商(项目)", "Facebook越南市场卖普洱茶和建水紫陶壶这个项目具体是做什么的?", "有没有在Facebook上卖云南普洱茶和建水紫陶的跨境项目?", "越南市场做普洱茶电商是不是用Facebook平台?", "建水紫陶壶出口越南,是通过Facebook做的吗?", "这个跨境电商项目名称叫什么?", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:罗有艳,2020年毕业于云南农业大学越南语专业。", "罗有艳是哪个公司的负责人?", "罗有艳学的是越南语专业吗?哪年毕业的?", "云南农业大学越南语专业是本科吗?罗有艳是2020届的吗?", "罗有艳负责的项目跟越南有关吗?", "罗有艳毕业于云南农大哪个学院?", "4. 入驻企业完整名录(39家/信息表)\n- 简介:依托Facebook全域流量生态(主页、直播、短视频、广告),聚焦越南垂直市场,主营普洱茶、手工茶具(云南建水紫陶小品壶及公道杯组合)、中式茶礼套装跨境零售。借助“内容种草+直播转化+9610跨境报关+越南专线直邮”闭环模式。", "园区里做普洱茶和建水紫陶跨境生意的是哪家公司?", "有没有专门做越南市场茶叶和茶具出口的企业?用的是Facebook直播带货那种模式吗?", "入驻企业里谁在搞‘内容种草+直播转化+9610报关+越南专线直邮’这一套跨境闭环?", "卖云南手工茶具、普洱茶,还主打越南市场的公司有几家?都在哪个园区?", "我想找做中式茶礼套装跨境零售的团队,他们是不是靠Facebook主页、短视频和广告引流?", "**鬼才明AI创意工作室(项目)**", "鬼才明AI创意工作室(项目)", "鬼才明AI创意工作室是干啥的?名字挺特别,是正式注册的项目吗?", "园区里有个叫‘鬼才明’的AI工作室,具体做什么方向?接不接企业定制开发?", "听说有个‘鬼才明AI创意工作室’,是学生创业项目还是老师带队的?", "‘鬼才明AI创意工作室’属于哪个企业或团队?有没有官网或联系方式?", "园区项目列表里的‘鬼才明AI创意工作室’,主要提供哪些AI相关服务?", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:赵广明,2022年毕业于重庆大学计算机专业。", "入驻企业里负责人叫赵广明、学计算机的那家是做什么的?", "赵广明是哪家企业负责人?重庆大学计算机系2022届毕业的,现在在园区做什么业务?", "园区里哪个公司的老板是重大的计算机毕业生,叫赵广明?", "赵广明负责的企业主营什么?他是不是刚毕业就创业了?", "查一下入驻企业名单,负责人姓名是赵广明、专业是计算机、毕业年份2022年的公司信息?", "4. 入驻企业完整名录(39家/信息表)\n- 简介:2026年4月启动,聚焦产教融合、创新创业赛事运营与数智化品牌赋能,围绕AI技术商业化落地、新商科专业规划、全品类活动策展及职业技能人才培育开展服务。搭建AI智能交互体系与AI数字人内容输出矩阵,为政校企提供AI场景应用、全域获客、职业技能培训、赛事集训、资格认证、品牌全案、项目申报等全链路解决方案。", "2026年4月启动的那个产教融合AI项目,具体服务政校企哪些方面?", "有没有做AI数字人内容矩阵、还帮学校搞新商科专业规划的公司?", "园区里提供AI职业技能培训、赛事集训和资格认证的机构是哪家?", "哪个团队能帮政府或高校做AI场景应用落地、全域获客和品牌全案?", "聚焦AI商业化落地、数智化品牌赋能,还搞创新创业赛事运营的是哪家入驻企业?", "**昆明云韵体育有限责任公司 / 男子自行车洲际队(项目)**", "昆明云韵体育有限责任公司 / 男子自行车洲际队(项目)", "昆明云韵体育公司是不是管男子自行车洲际队的?", "园区里有支男子自行车洲际队,背后运营公司叫啥名字?", "‘昆明云韵体育有限责任公司’除了体育培训,还运营专业自行车队吗?", "想了解园区内职业自行车队的情况,归属哪家公司?是洲际级别吗?", "云韵体育这家公司是干啥的?听说他们带一支男子自行车洲际队?", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:舒江贵,2020年7月毕业于麒麟区职业学校运动训练专业。", "入驻企业名单里那个舒江贵是谁?他什么时候毕业的?学的什么专业?", "舒江贵是园区哪家企业的负责人?他是不是麒麟区职业学校毕业的?", "运动训练专业毕业的舒江贵,在园区负责哪家企业?", "园区入驻企业负责人舒江贵的学历背景是什么?", "舒江贵是哪年从麒麟区职业学校毕业的?学的是运动训练专业吗?", "4. 入驻企业完整名录(39家/信息表)\n- 简介:2026年4月发起,拟在云南注册成立UCI男子自行车洲际队,打造立足云南、辐射全国、征战亚洲赛场的职业自行车竞技项目,依托云南高原训练优势,构建“职业赛事+商业赞助+高原训练+体育文旅+青苗人才培养”一体化运营模式。", "男子自行车洲际队项目是干什么的?准备什么时候成立?", "云南要建的UCI男子自行车洲际队,具体怎么运营?", "这个自行车队项目是不是要在云南注册?目标是打亚洲比赛吗?", "听说有个高原训练+体育文旅+青苗培养的一体化自行车项目,是哪个?", "男子自行车洲际队项目靠不靠谱?它怎么把职业赛事、商业赞助和文旅结合在一起?", "> 注:申报名单(第5节)将该“男子自行车洲际队项目”归类为 OPC。\n---", "‘男子自行车洲际队项目’在申报材料里被归到哪一类了?", "OPC是什么意思?自行车队项目为什么标着OPC?", "第5节申报名单里,自行车队项目属于OPC类,这个分类对入驻有影响吗?", "OPC类项目包括哪些?自行车队项目为啥被划进OPC?", "园区里还有哪些项目和自行车队一样属于OPC类别?", "> 出处:附件1.3 创业孵化载体入驻实体名单(省级申报口径,31家)\n| 序号 | 入驻经营实体或项目团队名单 |\n|---|---|\n| 1 | 昆明舒护安养老服务有限责任公司 |\n| 2 | 星杭商务信息咨询(昆明)有限责任公司 |\n| 3 | 达岸教育管理(云南)有限公司 |\n| 4 | 五华区丽裳文化艺术工作室 |\n| 5 | 米勒克尔蓝宝石(项目) |\n| 6 | 中越生物合作——高纯度鲎血蛋白提取的跨境技术创新和商业推广(项目) |\n| 7 | 云南宸中低空经济有限公司 |\n| 8 | 中泰教育合作(项目) |\n| 9 | 人工智能机器人训练(项目) |\n| 10 | 欧森纳东盟香港留学(项目) |\n| 11 | 仰光客厅中缅文化体验营地(项目) |\n| 12 | PineSound派音智能——下一代音效与配乐基座(项目) |\n| 13 | 千楣之路——缅甸“千年之木”Thanaka非遗文化传播与跨境贸易(项目) |\n| 14 | 昆明朵哈食品有限公司 |\n| 15 | 滇缅一带一路国际化建设设计(项目) |\n| 16 | 云南定影文化传播有限公司 |\n| 17 | 酷享野农AI农业 |\n| 18 | 昆明智海银高文化科技有限公司 |\n| 19 | 鬼才明AI创意策划工作室(项目) |\n| 20 | 昆明就惯你生物科技(项目) |\n| 21 | 联翼航空技术低空经济应用国际化拓展(项目) |\n| 22 | 中外青少年研学交流(项目) |\n| 23 | 昆明瀚颖教育信息咨询有限公司 |\n| 24 | 花仙子园艺肥料(云南)有限公司 |\n| 25 | 云南星瑞航空科技(项目) |\n| 26 | 男子自行车洲际队项目(OPC) |\n| 27 | 云南廷秀文旅康养服务项目(OPC) |\n| 28 | 中通乐创(项目) |\n| 29 | 跨境电商出口运营项目 |\n| 30 | 华寮国际教育交流(云南)有限责任公司 |\n| 31 | 综合性云南直播私域服务平台 |\n> 说明:本名单项目名称部分与《入驻企业信息表》略有出入(如“米勒克尔蓝宝石”、", "附件1.3里的31家入驻实体名单完整版能给我看看吗?", "省级申报口径的入驻名单有31家,都有哪些公司或项目?", "‘米勒克尔蓝宝石’‘中越生物’这些项目在31家名单里排第几?", "园区入驻的31家单位里,有没有叫‘男子自行车洲际队项目’的?排多少号?", "‘仰光客厅中缅文化体验营地’‘PineSound派音智能’这些项目都在31家名单里吗?", "“欧森纳东盟香港留学”、中越生物改名等),系两表不同填报口径/时点所致,均照原文保留。\n---", "为什么同一个项目在不同表格里名字不一样?比如‘欧森纳东盟香港留学’和‘中越生物’改名了?", "入驻企业信息表和附件1.3名单名字对不上,是填错了还是有别的原因?", "两份名单项目名称有出入,比如‘米勒克尔蓝宝石’,是因为填报时间不同吗?", "园区不同表格里项目名称不一致,是口径不同还是数据没同步?", "‘中越生物’在两个表里名字不一样,是不是因为改名了?官方以哪个为准?", "> 出处:昆明市大学生创业园创业企业(项目)入驻管理规程.wps(昆明市人力资源和社会保障局,2026年3月13日)", "昆明市大学生创业园的入驻管理规程是哪个部门发布的?", "这份《昆明市大学生创业园创业企业(项目)入驻管理规程》是哪年哪月哪日发布的?", "《昆明市大学生创业园创业企业(项目)入驻管理规程》的发布单位是哪个?", "这个管理规程的出处文件名是什么?", "2026年3月13日发布的园区入驻管理文件全称是什么?", "### 一、实施主体", "昆明市大学生创业园的主管部门是哪个单位?", "谁在管昆明市大学生创业园的入驻事务?", "负责统筹昆明市大学生创业园入驻工作的政府部门是哪家?", "昆明市人社局是不是创业园的主管单位?", "园区入驻管理归哪个局管?", "6. 企业(项目)入驻管理规程要点\n- 运营主管:昆明市人力资源和社会保障局(主管部门)", "昆明市大学生创业园的入驻流程由谁具体操作?", "实际帮创业者办入驻手续的是什么机构?", "政府委托哪家单位来执行入驻全流程服务?", "入驻申请找谁办?是人社局直接办还是外包给别的公司?", "第三方服务机构在园区入驻中具体干些什么?", "6. 企业(项目)入驻管理规程要点\n- 执行:通过政府购买服务委托第三方服务机构(服务机构),负责入驻全流程工作;主管部门行使监督、考核、验收职权。", "昆明市人社局在入驻管理中只负责监督考核吗?", "主管部门除了监督,还参与入驻审核或审批吗?", "人社局对第三方服务机构怎么管?", "验收入驻工作是谁来做的?", "监督、考核、验收这三项职责都是人社局的吗?", "### 二、入驻条件", "想进昆明市大学生创业园创业,要满足哪些硬性条件?", "申请入驻昆明市大学生创业园需要符合哪些要求?", "园区入驻门槛有哪些?", "大学生创业项目进园区得达到什么标准?", "入园资格审查主要看哪些方面?", "6. 企业(项目)入驻管理规程要点\n- **对象范围**:在校大学生及毕业8年内高校毕业生、归国留学生、外籍留学生等青年创业企业(项目),申请人须担任法定代表人/项目负责人。符合昆明市产业发展的优质企业(项目)或OPC企业可适当放宽。", "我是刚毕业两年的大学生,想创业开公司,能申请入驻园区吗?", "留学生回国创业,没在国内上过大学,可以入驻这个园区吗?", "我毕业已经7年了,还能算作符合条件的青年创业者吗?", "我们公司是外国企业,但想在昆明落地项目,能申请入驻吗?", "如果我不是法人,只是团队负责人,能代表项目申请入驻吗?", "6. 企业(项目)入驻管理规程要点\n- **产业方向**:聚焦昆明市“10+N”产业链——数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等,具备创新性、可操作性、市场前景良好。", "园区重点支持哪些行业的创业项目?", "做绿色农产品电商的项目符合园区的产业方向吗?", "搞人工智能应用开发的公司属于园区鼓励的数字经济范畴吗?", "文化旅游和花卉相关的创业项目,园区会优先考虑吗?", "新能源汽车零部件研发算不算新材料或新能源产业链里的项目?", "6. 企业(项目)入驻管理规程要点\n- **其他要求**:明确可行的经营计划、启动资金和风险承担能力,规范的企业管理制度;无知识产权纠纷、违法犯罪记录、经营异常记录;守法经营、诚实守信,遵守园区各项规定。", "入驻需要提交详细的商业计划书吗?", "启动资金有没有最低要求?没有实际出资能申请吗?", "公司之前有轻微的税务异常记录,还能申请入驻吗?", "团队有成员曾涉及知识产权官司,会影响整个项目入驻吗?", "园区对企业的内部管理制度有具体检查标准吗?比如必须有财务制度或人事规章?", "6. 企业(项目)入驻管理规程要点\n- **优先入驻**:获得市级及以上创业大赛奖项、突出学术成就、重大发明创造及高新专利技术的企业(项目)。", "参加过‘春城创业荟’拿了二等奖,入驻时有加分或优先权吗?", "我们团队有国家发明专利,但没得过奖,算优先入驻对象吗?", "博士期间发了多篇顶刊论文,算‘突出学术成就’吗?", "获得省级创新创业大赛铜奖,符合优先入驻条件吗?", "手上有2项实用新型专利,能算重大发明创造吗?", "### 三、入驻流程", "入驻园区一共要走几个步骤?大概多久能办完?", "从提交申请到正式入驻,中间要经过哪些环节?", "线上能全程办理入驻手续吗?还是必须现场跑几次?", "入驻流程里需要找园区哪个部门对接?有专门的联系人吗?", "材料初审通过后,后续还有评审或答辩环节吗?", "6. 企业(项目)入驻管理规程要点\n1. **提出申请**:提交《入驻申请表》《创业计划书》、身份证(外籍提供护照/签证)、学历证明、营业执照(已注册)、资质专利证书等。", "申请园区入驻需要准备哪些材料?", "入驻园区要交哪些表格和证件?", "第一次申请园区入驻,得带身份证、学历证这些吗?", "创业公司还没注册营业执照,能申请园区入驻吗?", "提交入驻申请时,创业计划书和资质专利证书是必须的吗?", "6. 企业(项目)入驻管理规程要点\n2. **项目初审**:服务机构初审,材料不齐一次性告知补全。", "材料不全的话园区会怎么处理?", "初审没通过是不是就直接被拒了?", "服务机构初审时发现材料缺东西,会告诉我补什么吗?", "初审阶段如果漏交了某份文件,还能补交吗?", "园区初审是一次性指出所有问题,还是分几次通知?", "6. 企业(项目)入驻管理规程要点\n3. **入驻评审**:服务机构从《昆明市就业创业导师库》抽取3名评审专家,路演答辩评价,量化打分制(100分),80分及以上通过。", "入驻评审是怎么进行的?", "路演答辩环节具体怎么安排?", "评审专家是从哪里请来的?是昆明本地的吗?", "入驻打分满分100,多少分才算过关?", "评审结果是当场出还是等几天?80分以上是不是就稳了?", "6. 企业(项目)入驻管理规程要点\n- 团队资质与能力 **20分**(学历背景、专业匹配度、创业经验、组织管理、人员稳定)", "团队资质这块评分占多少分?", "评审时怎么看我们团队行不行?", "学历不高但有多年创业经验,团队分会不会很低?", "团队人员流动性大,会影响20分里的‘人员稳定’项吗?", "专业跟项目不完全对口,‘专业匹配度’这部分还能拿高分吗?", "6. 企业(项目)入驻管理规程要点\n- 项目创新性与技术壁垒 **25分**(核心技术/商业模式创新点、自主知识产权、技术领先性、行业壁垒)", "项目创新性这部分占多少分?", "没有专利但有独特商业模式,算不算有创新性?", "技术壁垒指的是什么?我们用的是成熟技术但做了优化,算吗?", "自主知识产权必须是发明专利才行吗?软著或实用新型可以吗?", "行业壁垒高但技术本身不新,创新性这25分还能拿满吗?", "6. 企业(项目)入驻管理规程要点\n- 产业适配与市场前景 **25分**(与重点产业链匹配度、市场需求、竞品、市场拓展、盈利模式)", "企业入驻评分里‘产业适配与市场前景’占多少分?这部分主要看哪些方面?", "匹配重点产业链、市场需求、竞品分析这些,是不是都算在25分的‘产业适配与市场前景’里?", "入驻打分中,市场拓展能力和盈利模式归在哪项?分值是多少?", "跟园区重点产业链贴合度不高,会影响入驻评分吗?这项占几分?", "‘产业适配与市场前景’25分具体怎么打分?需要提供竞品分析报告吗?", "6. 企业(项目)入驻管理规程要点\n- 运营规划与落地能力 **15分**(商业计划完整性、启动资金、经营管理制度、阶段目标、落地方案)", "运营规划和落地能力占多少分?商业计划书不完整会被扣分吗?", "启动资金和经营管理制度这些,属于评审里的哪一块?分值是多少?", "入驻打分中,有没有阶段目标和落地方案的要求?占多少分?", "光有想法没有详细执行计划,会影响入驻吗?对应的是哪项评分标准?", "‘运营规划与落地能力’15分,具体要提交哪些材料来证明?", "6. 企业(项目)入驻管理规程要点\n- 社会价值与发展潜力 **15分**(带动就业能力、税收贡献预期、行业潜力、可持续发展)", "社会价值与发展潜力占多少分?带动就业和税收预期各占多大比重?", "入驻评审里,行业潜力和可持续发展归在哪一项?分值是多少?", "如果项目能带动几十人就业,但短期税收不高,社会价值这块能得高分吗?", "‘社会价值与发展潜力’15分,是不是主要看能招多少人、交多少税?", "园区看重长期发展,那行业潜力和可持续发展在评分里怎么体现?分值多少?", "6. 企业(项目)入驻管理规程要点\n4. **评审公示**:报主管部门备案后,在市人社局官网公示,无异议准予入驻。", "入驻评审结果要在哪个网站公示?公示期多久?", "人社局官网公示后没人提意见,是不是就自动算通过了?", "评审完还要报哪个部门备案?备案之后才公示吗?", "公示环节是必须的吗?没公示能不能直接入驻?", "市人社局官网公示无异议后,下一步是什么流程?", "6. 企业(项目)入驻管理规程要点\n5. **签署协议**:签订《入驻孵化协议》,分配办公场地/工位,正式入驻。", "签《入驻孵化协议》是在公示之后还是之前?", "协议签完就能马上拿到工位吗?办公场地怎么分配?", "正式入驻的标志是签协议还是拿到工位?", "《入驻孵化协议》签了以后,多久能安排办公场地?", "签完协议、分到工位,是不是就代表可以正式入驻开始办公了?", "### 四、入驻周期(梯度孵化四大区域)\n| 孵化区域 | 面向对象 | 配套 | 孵化周期 |\n|---|---|---|---|\n| OPC创业空间 | 以AI为支撑、聚焦数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域 | 拎包入驻的独立办公位或共享工位 | 最长2年 |\n| 创业苗圃区 | 创意阶段、未注册企业的大学生创业团队 | 开放式共享工位 | 最长2年(落地成长好可推荐转加速区1年) |\n| 孵化加速区 | 已完成工商注册、", "OPC创业空间适合哪些类型的创业项目?能待多久?", "大学生没注册公司,能在园区哪个区域孵化?最长能待几年?", "创业苗圃区和加速区有什么区别?没注册的团队能进加速区吗?", "国际创客区是给谁用的?配套服务跟其他区一样吗?", "归国留学生创业,能申请哪个孵化区域?入驻时间怎么算?", "处于初创期的企业(项目) | 独立办公空间 | 基础2年,评估合格可延1年,累计≤3年 |\n| 国际创客区 | 归国留学生、外籍留学生创业企业(项目) | 个性化办公空间+国际化配套服务 | 参照孵化加速区 |", "孵化加速区对企业注册状态有要求吗?最多能待几年?", "初创企业申请加速区,需要提供营业执照吗?最长能续多久?", "已注册的初创公司,适合申请哪个孵化区域?能住满3年吗?", "加速区和国际创客区的入驻时长一样吗?国际创客区也要注册公司吗?", "国际创客区的‘个性化办公空间’具体指什么?跟加速区的独立办公空间一样吗?", "### 五、项目年度考核", "园区每年怎么考核入驻企业?考核内容包括哪些方面?", "入驻企业每年要参加什么考核?考核标准是怎么定的?", "园区对创业项目的年度评估都看哪些指标?有没有打分规则?", "企业年度考核会查营收、专利这些吗?还有别的考核点吗?", "园区怎么判断一家企业是不是适合继续留在孵化器里?", "6. 企业(项目)入驻管理规程要点\n- **考核维度**(量化打分制,100分):经营发展能力(20):营收/利润/纳税、经营计划完成度、管理制度完善度;技术创新能力(25):知识产权新增、研发投入、产品/服务迭代、产学研合作;合规与履约情况(20):遵守管理制度、协议履约、场地规范、材料报送、守法经营;创业带动成效(20):新增就业(高校毕业生占比)、实习实训岗位;发展潜力与孵化成效(15):融资进展、资质认证、市场拓展、可持续规划、出孵成熟度。", "经营发展能力在考核里占多少分?具体看哪些指标?", "技术创新能力考核包括研发投入和产学研合作吗?分值是多少?", "合规履约情况具体考什么?比如材料报送不及时会扣分吗?", "创业带动成效是不是要看招了多少应届生?实习岗位也算分吗?", "发展潜力这块主要看融资和资质认证?满分15分怎么打?", "6. 企业(项目)入驻管理规程要点\n- **考核等级**:优秀(80分及以上)、合格(60–79分)、不合格(60分以下)。", "年度考核多少分算合格?不合格会怎么样?", "考核80分以上就是优秀?有没有奖励或优先续签?", "如果考核只有59分,会被清退吗?有没有补救机会?", "考核结果分几个等级?每个等级对应什么后续安排?", "园区对考核不合格的企业会直接赶出去,还是先提醒整改?", "6. 企业(项目)入驻管理规程要点\n- **结果处理**:合格准予继续孵化(期满可申请延期);不合格下达《整改通知书》,整改期满仍不合格的可解除孵化协议、勒令限期退出。", "企业孵化不合格会被赶出去吗?", "孵化不合格会收到什么通知?", "整改后还是不合格,园区会怎么处理?", "整改通知书是什么意思?", "孵化协议会被解除吗?", "### 六、退出管理(四种类型)", "园区企业退出有哪几种情况?", "企业退出园区分哪些类型?", "退出管理包括哪些情形?", "企业从园区离开有哪些方式?", "园区对退出是怎么分类的?", "6. 企业(项目)入驻管理规程要点\n1. **成功孵化出园**:满足任一成熟出孵标准——①产权明晰,有自主核心产品并规模化;②经营良好,产品有稳定市占率与盈利;③制度健全、组织完备、团队稳定;④已获行业经营资质、质量稳定;⑤有持续研发能力与规划。", "什么情况下算成功毕业出园?", "企业满足哪些条件就能出园了?", "出孵标准具体有哪些?", "怎么才算孵化成熟可以离开?", "自主核心产品规模化是不是出园条件之一?", "6. 企业(项目)入驻管理规程要点\n2. **孵化期满退出**:周期届满未申请延期或延期未通过、已延期到期。", "孵化期满了不续签会怎样?", "孵化到期没申请延期怎么办?", "延期申请没通过是不是就得走?", "已经延期过一次,到期了还能再延吗?", "孵化合同到期不续,园区会让我搬走吗?", "6. 企业(项目)入驻管理规程要点\n3. **主动申请退出**:因经营问题、团队变动等申请退出。", "企业自己想退出园区要怎么办?", "因为经营困难能主动申请退出吗?", "团队散了还能申请退出吗?", "主动退园需要走什么流程?", "不想继续孵化了,怎么向园区提出退出?", "6. 企业(项目)入驻管理规程要点\n4. **强制勒令退出**:①协议签订后累计30个工作日无人到现场办公,或连续7个工作日空置且未报备累计超3次;②违反规定造成重大经济损失/名誉损失;③不服从管理情节严重;④被停业、吊销执照、破产清算;⑤违法经营;⑥私自转租、变更经营项目;⑦虚报数据、伪造材料、拒不配合。", "企业入驻后多久没人办公会被强制清退?", "哪些情况会导致园区勒令企业退出?比如空置、违规、转租这些算吗?", "企业被吊销营业执照或者破产了,园区会怎么处理?", "不配合检查、虚报数据或者擅自改项目,会被赶出去吗?", "连续几天办公室没人,或者一个月内累计多少天没人办公就算违规?", "> 出处:空间布局.png(概念布局)与 详细的布局.png(分区布置图)\n**概念布局(空间布局.png)**:顶部左侧为“玻璃顶”(开敞中庭/天光区);上部右侧以“过道”纵贯,两侧各若干“房间”;中上部为“大厅”;中部横向依次为若干“房间”、橙色的“共享工位区”、绿色的“休息厅”;底部由完整“过道”贯通,下接一排9间“房间”。", "园区平面图里那个带玻璃顶的地方是哪里?", "中庭、共享工位区和休息厅在图上分别是什么颜色、在什么位置?", "整个楼层布局是怎么安排的?比如大厅、过道、房间这些怎么分布的?", "顶部左侧的‘玻璃顶’和中上部的‘大厅’是不是同一个区域?", "最下面一排有9个房间,它们是连着一条过道的吗?", "**分区布置(详细的布局.png)**:\n| 区域类别 | 编号/名称 | 数量 |\n|---|---|---|\n| 成长区 | 成长区01–08 | 8 |\n| 国际区 | 国际区01–07 | 7 |\n| 加速区 | 加速区01–13 | 13 |\n| 共享办公 | 共享工位区 | 1 |\n| 会议空间 | 小会议室 / 大会议室 | 2 |\n| 功能空间 | 玻璃顶(中庭)、休息厅、过道、", "分区布置(详细的布局.png)", "成长区、国际区、加速区各有多少间办公室?", "共享工位区、大小会议室和玻璃顶这些算不算独立分区?数量怎么算?", "‘OPC’在布局图里对应哪个区?和宣传材料里的分区命名一致吗?", "休息厅、过道、玻璃顶这些功能空间有没有编号或具体数量?", "详细的布局图里提到的‘楼道’有多少条?分布在哪些位置?", "楼道 | 若干 |\n> 布局总览:上部左侧为“玻璃顶”开敞区,右上依次为成长区与国际化两纵列;中部靠上为共享工位区与休息厅;下方以“过道”为轴,分布加速区、大小会议室及楼道。分区命名与《入驻企业信息表》中的“加速/国际/成长/OPC”场地安排完全对应。\n---", "玻璃顶、成长区、国际区这些分区在楼层里是怎么上下左右排列的?", "共享工位区和休息厅是在楼层中间偏上的位置吗?", "加速区是排在最下面一排还是沿着过道分布的?", "大小会议室和加速区都在过道附近吗?它们和楼道的关系是什么?", "《入驻企业信息表》里的‘加速/国际/成长/OPC’分类,在实际布局里怎么一一对应的?", "> 出处:昆明市创业园宣传册.pdf(16页彩页;以下为正文要点,图片部分略)", "昆明市创业园宣传册一共多少页?主要内容有哪些?", "宣传册里有没有介绍园区的空间布局或者分区类型?", "16页彩页的宣传册里,图片部分包含哪些关键信息?", "宣传册PDF文件里有没有提到企业入驻流程或管理规定?", "这份宣传册是官方发布的吗?主要面向哪些人群?", "### 8.1 园区定位与理念", "园区的定位和核心理念是什么?", "这个园区主要想实现什么目标?", "园区倡导的理念有哪些?", "园区是怎么定义自己的发展方向的?", "园区的整体定位和愿景是什么?", "8. 宣传册核心内容(园区简介·服务·政策)\n- 云南省首家由政府部门主办、为创业青年大学生提供**免费创业孵化服务**的**开放式园区**。", "这个园区是免费的吗?", "大学生创业能在这里得到免费孵化服务吗?", "是不是政府办的创业园区?", "有没有专门针对青年大学生的创业孵化基地?", "这是不是云南第一家政府主办的大学生创业孵化园?", "8. 宣传册核心内容(园区简介·服务·政策)\n- 2009年正式成立,昆明市人力资源和社会保障局主办运营,2012年获评省级创业园;位于民航路229号昆明市人力资源中心1–2楼,总面积约3000平方米。", "园区是哪一年成立的?", "昆明市人社局主办的创业园什么时候开始运营的?", "这个园区在2012年获得了什么称号?", "园区具体地址在哪里?在几楼?", "园区总面积有多大?在昆明哪个位置?", "8. 宣传册核心内容(园区简介·服务·政策)\n- 以助力青年大学生创业梦想为核心,构建 **“空间+孵化+融资+政策+资源+AI赋能+综合服务”七位一体**全链条创业扶持体系,打造“一站式”创业孵化服务平台。", "园区提供哪些创业支持服务?", "什么是‘七位一体’创业扶持体系?", "园区能帮创业者解决哪些问题?", "有没有AI相关的创业支持?", "园区能不能提供融资、政策、资源等一站式服务?", "8. 宣传册核心内容(园区简介·服务·政策)\n- 紧扣面向南亚东南亚的国际定位,融合AI前沿技术,打造现代化创业/创新服务平台,成为青年人才来昆留昆创业创新的“首站”与“家园”。", "园区面向南亚东南亚有什么特别规划吗?", "园区怎么体现国际化定位?", "AI技术在园区服务中怎么应用?", "来昆明创业的年轻人,这里是不是第一站?", "园区是不是专门为来昆明创业就业的青年打造的‘家园’?", "### 8.2 政策服务(七大板块)", "园区政策服务包括哪些主要板块?", "政策服务一共有几大类?", "宣传册里提到的政策服务有哪七个部分?", "园区的政策服务具体分哪几块?", "8. 宣传册核心内容(园区简介·服务·政策)\n1. **空间保障**:免费办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等基础办公设施。", "园区提供免费办公场地吗?", "新创企业能申请免费工位或独立办公室吗?", "办公桌椅、文件柜、高速网络这些基础设备园区包不包?", "有没有免费的基础办公设施可以使用?", "初创团队能拿到免费办公空间吗?", "8. 宣传册核心内容(园区简介·服务·政策)\n2. **孵化服务**:专业创业指导专家团队提供创业规划、技术指导、商业模式梳理、市场对接等“一对一”服务;定期举办创业沙龙、创业讲堂、项目路演。", "园区有没有创业指导服务?", "有没有一对一的创业辅导?", "园区会帮创业者梳理商业模式和对接市场吗?", "创业沙龙、项目路演这些活动定期办吗?", "有没有专家团队帮我们做技术指导和创业规划?", "8. 宣传册核心内容(园区简介·服务·政策)\n3. **融资支持**:联动省内外创投机构,设立创业种子资金/合作投资基金,提供政策性贷款、商业银行贷款、股权融资等推荐指导。", "园区能帮忙找投资吗?", "有没有种子资金或者合作基金可以申请?", "园区能推荐银行贷款或股权融资渠道吗?", "创业初期怎么获得融资支持?", "园区跟创投机构有合作吗?", "8. 宣传册核心内容(园区简介·服务·政策)\n4. **政策对接**:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策。", "园区能帮忙申请创业补贴吗?", "税收减免和社保补贴这些政策园区能协助办理吗?", "高校毕业生创业有哪些扶持政策,园区能帮着落实吗?", "创业扶持资金怎么申请?园区会协助吗?", "政策对接服务具体都包括哪些内容?", "8. 宣传册核心内容(园区简介·服务·政策)\n5. **资源整合**:对接驻昆高校、科研院所、龙头企业,提供技术合作、成果转化、人才招聘;纳入昆明创业创新服务体系,优先参与“春城创业荟”等品牌活动。", "园区能帮我们对接高校和科研院所吗?", "如果想跟昆明的大学或大公司合作技术或者招人,园区能帮忙吗?", "园区是不是属于昆明创业创新服务体系?能参加‘春城创业荟’这类活动吗?", "我们项目想做成果转化,园区能提供对接支持吗?", "园区有没有资源帮初创企业找技术合作伙伴或者引进人才?", "8. 宣传册核心内容(园区简介·服务·政策)\n6. **专项赋能培训**:定期开展AI公益性培训,提供AI应用、数字内容创作、一人公司创业落地、企业AI降本增效等公益培训与实战辅导。", "园区有没有免费的AI培训?都教些什么内容?", "想学怎么用AI做内容创作或者开一人公司,园区有相关培训吗?", "听说园区有AI公益课,能教企业怎么用AI省钱增效吗?", "园区的AI培训是实操性质的吗?有没有老师手把手教?", "除了AI应用,园区还开哪些创业相关的免费培训课?", "8. 宣传册核心内容(园区简介·服务·政策)\n7. **其他服务**:提供工商注册、税务代办、知识产权咨询、法律援助等专业化服务。", "注册公司的事园区能代办吗?", "刚创业,税务和知识产权这些专业事园区能帮忙处理吗?", "园区提供法律援助服务吗?比如合同纠纷或者劳动问题?", "工商注册、报税这些流程复杂,园区有专人协助办理吗?", "知识产权咨询是免费的吗?包括商标和专利方面的指导?", "### 入驻条件(宣传册口径,与管理规程一致)\n(一)**对象范围**:在校大学生及毕业8年内高校毕业生、归国留学生、外籍留学生等青年创业企业(项目),申请人须担任法定代表人/项目负责人;优质项目或OPC企业可放宽。\n(二)**产业方向**:聚焦昆明市“10+N”产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等),具备创新性、可操作性、市场前景良好。\n(三)**其他要求**:明确可行的经营计划、", "在校大学生能申请入驻园区吗?", "毕业6年的本科生还能以创业者身份申请入园吗?", "外籍留学生创业,符合园区的入驻条件吗?", "园区重点支持哪些行业的创业项目?比如做生物医药或者新能源的行不行?", "我们项目是做数字文旅的,属于昆明‘10+N’产业链里吗?", "启动资金和风险承担能力,规范管理制度;无知识产权纠纷、违法犯罪及经营异常记录。\n(四)**优先入驻**:获得市级及以上创业大赛奖项、突出学术成就、重大发明创造及高新专利技术的项目。", "入驻需要提交详细的商业计划书吗?", "启动资金要多少才够?园区对创业者的资金能力有硬性要求吗?", "公司之前没出过问题,但没申请过专利,会影响入驻吗?", "如果团队有发明专利或者拿过市级创业比赛奖,是不是更容易入园?", "园区查不查企业的信用记录?比如有没有违法或经营异常?", "### 入驻流程(宣传册六步)\n咨询报名 → 提出申请 → 项目初审 → 入驻评审(路演答辩,量化打分≥80分通过)→ 评审公示(市人社局官微公示)→ 签署协议、分配场地工位、正式入驻。\n---", "入驻园区要走哪些步骤?整个流程是怎样的?", "从咨询到正式入驻,一共要经过哪几个环节?", "路演答辩后多少分才算通过?评审结果在哪公示?", "入驻评审要答辩吗?有没有打分标准?多少分能过?", "市人社局官微会公示哪些信息?公示期多久?", "| 区域 | 对象 | 配套 | 周期 |\n|---|---|---|---|\n| **OPC创业空间** | AI为支撑,聚焦数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域 | 拎包入驻独立办公位或共享工位 | 最长2年 |\n| **创业苗圃区** | 创意阶段、未注册企业的大学生创业团队 | 开放式共享工位 | 最长2年(成长好可转加速区1年) |\n| **孵化加速区** | 已工商注册、", "OPC创业空间适合做AI应用的团队吗?能直接拎包入住吗?", "我们是还没注册公司的大学生团队,能申请哪个区?共享工位能用多久?", "创业苗圃区待满两年后,表现好还能继续留园吗?怎么转区?", "孵化加速区给初创企业配的是独立办公室吗?最长能待几年?", "留学生回国创业,该申请哪个区域?配套和普通区一样吗?", "初创期企业 | 独立办公空间 | 基础2年,评估合格延1年,累计≤3年 |\n| **国际创客区** | 归国留学生、外籍留学生 | 个性化办公空间+国际化配套 | 参照孵化加速区 |\n---", "国际创客区和孵化加速区的入驻时长规定是一样的吗?", "归国留学生申请国际创客区,办公空间是定制的吗?有哪些国际化配套?", "孵化加速区的企业续期需要什么条件?最多能待三年对吗?", "初创企业已经注册了,但刚成立一个月,符合孵化加速区要求吗?", "创业苗圃区和孵化加速区在工位类型上有什么区别?", "11. 报名方式\n- 咨询电话:18087174891、18214573650(宣传册);18087174891(统计表填报人);云南省就业局/园区申报口径见第3节填报信息", "咨询电话是多少?打哪个号问入驻报名最靠谱?", "18087174891这个号码是打给谁的?报统计表也打这个吗?", "云南省就业局那边怎么申报?和园区直报流程一样吗?", "报名咨询除了打电话,还有别的联系方式吗?比如微信或网站?", "填报人电话和宣传册上的咨询电话重复了,是不是同一个联系人?", "11. 报名方式\n- 咨询地址:昆明市民航路229号(昆明市人力资源中心二楼 昆明市大学生创业园)", "园区办公地址在哪?具体到门牌号和楼层?", "昆明市大学生创业园是在昆明市人力资源中心里面吗?", "去现场咨询要上几楼?是在民航路229号的二楼吗?", "人力资源中心二楼除了创业园,还有其他办事窗口吗?需要预约吗?", "导航搜‘昆明市大学生创业园’能准确定位到民航路229号吗?", "11. 报名方式\n- 接收邮箱:714066050@qq.com", "报名材料发到哪个邮箱?", "报名要发邮件到什么地址?", "接收报名的邮箱是多少?", "报名是通过邮箱提交吗?", "714066050@qq.com 是报名用的邮箱吗?", "**媒体报道矩阵**:整合电视、广播、报纸、新媒体等资源,通过电视专栏、广播专题、报纸专版、网络直播、地铁专列、新媒体推广等方式,全方位宣传报道园区创业创新典型事迹。\n**合作媒体机构**(宣传册列示):", "媒体报道矩阵", "园区都跟哪些媒体合作做宣传?", "园区怎么对外宣传创业典型?", "电视、广播、报纸这些渠道园区都用吗?", "地铁专列和网络直播也是园区的宣传方式吗?", "园区的媒体报道覆盖哪些平台类型?", "12. 媒体报道与合作媒体\n- 全国主流:央视网(CCTV.com)、中国网(China.org.cn)、人民网(people.cn)、今日头条、网易、搜狐、抖音", "园区合作的全国性媒体有哪些?", "央视网、人民网这些算不算园区的合作媒体?", "抖音和今日头条是不是园区的全国合作媒体?", "网易搜狐这些大平台跟园区有宣传合作吗?", "CCTV.com和people.cn是园区对接的主流媒体吗?", "12. 媒体报道与合作媒体\n- 省级媒体:云南日报、云南广播电视台(Yunnan Media Group)、云南网、春城晚报", "云南本地的媒体哪些跟园区有合作?", "云南日报和云南台是不是园区合作的省级媒体?", "春城晚报和云南网也在园区的合作名单里吗?", "Yunnan Media Group指的是云南广播电视台吗?", "园区在云南省内主要跟哪些媒体联动?", "12. 媒体报道与合作媒体\n- 市级媒体:昆明广播电视台、昆明日报、掌上春城(KMCC)、无线昆明", "昆明市有哪些媒体和园区合作?", "昆明日报和昆明广播电视台是不是园区的市级合作媒体?", "掌上春城和无线昆明是园区合作的本地平台吗?", "KMCC是不是指掌上春城?它跟园区有宣传合作吗?", "园区在昆明市主要通过哪些媒体做报道?", "12. 媒体报道与合作媒体\n- 其他:8099999 新闻热线等", "园区的新闻热线是多少?", "想联系园区发新闻稿,电话是多少?", "8099999这个号码是园区的媒体热线吗?", "打哪个电话可以对接园区的媒体合作?", "园区对外的新闻联络方式有哪些?", "**重要外宣数据口径(宣传册/新闻引用,酌情采用)**:", "重要外宣数据口径(宣传册/新闻引用,酌情采用)", "园区宣传册上用的数据口径是哪些?", "对外宣传时园区推荐用哪些官方数据?", "新闻报道里提到的园区数据,有没有统一的标准说法?", "园区有没有指定的对外宣传数据模板或参考口径?", "做宣传材料时,哪些园区数据是官方认可、可以引用的?", "12. 媒体报道与合作媒体\n- 昆明市政府网站:园区运营超过10年,入驻企业超1000家、带动创业者超1万人次(2024-04报道口径)。", "昆明市政府网站上怎么介绍园区的规模和成效?", "园区运营多久了?有多少企业入驻?带动多少创业者?", "昆明市政府官网说园区入驻企业超1000家,这数据是截至什么时候的?", "园区运营超过10年,带动创业者超1万人次,这个说法出自哪里?", "昆明市政府网站提到的园区数据(比如1000+企业、1万+创业者)具体是哪篇报道的口径?", "12. 媒体报道与合作媒体\n- 春城晚报:近3年累计孵化企业超100家,培育国家级高新技术企业5家、省级科技型中小企业12家。", "春城晚报报道过园区孵化企业的成绩吗?", "园区近3年孵化了多少家企业?培育了多少高新和科小企业?", "春城晚报说园区孵化超100家企业,其中5家是国家级高新,12家是省级科小,这准确吗?", "有没有媒体报道过园区在培育高新技术企业方面的成果?", "春城晚报提到的园区企业培育数据(如5家国高、12家省科小)具体指什么?", "12. 媒体报道与合作媒体\n- 昆明日报:2024年以来新增入驻企业32家、其中高新技术企业15家;举办各类活动126场,覆盖青年创业者超2万人次。", "昆明日报最近报道过园区新增企业情况吗?", "2024年以来园区新入驻了多少家企业?其中高新企业有多少?", "园区今年办了多少场活动?覆盖多少青年创业者?", "昆明日报说园区2024年新增32家企业、举办126场活动,这些数据能确认吗?", "昆明日报提到的‘2024年以来新增高新技术企业15家’,是指新认定还是新入驻?", "12. 媒体报道与合作媒体\n- **提示**:以上为媒体报道口径,与省级申报/月报表中的官方累计口径(累计孵化181家、出园149家、带动就业1529人等)存在差异,对外引用时请注明来源。", "媒体报道里说的孵化企业数量跟政府报表上的数字不一样,这是怎么回事?", "对外宣传时提到的孵化企业数和官方统计的累计数据有出入,该怎么正确引用?", "为什么媒体报道口径和省级申报、月报表里的累计数据不一致?", "园区对外发布的孵化企业数、出园企业数、带动就业人数这些数据,是不是和官方统计有差别?", "媒体报道中用的那些数据,比如孵化181家,跟人社局正式报表里的数字对不上,哪个更权威?", "| 时间 | 事件 |\n|---|---|\n| 2009年9月 | 昆明市大学生创业园成立 |\n| 2012年 | 获评省级创业园 |\n| 2024-04-15 | 昆明市大学生创业园2026年第二期入驻招募公告发布(同时发布第一期评审结果公示) |\n| 2024-04-27前后 | 园区经全周期优化后“焕新亮相”获多家媒体报道(央视网5G、中国网、昆明市政府网、昆明日报、", "昆明市大学生创业园是哪年成立的?", "园区什么时候被评为省级创业园的?", "2024年4月园区焕新亮相是怎么回事?都有哪些媒体报了?", "央视网5G、中国网这些大媒体啥时候报道了园区焕新?", "园区2024年4月15日发布的第二期入驻招募公告,跟第一期评审结果公示是一起发的吗?", "8099999等),报道时间多为2024-04-28 |\n| 2024-05-08 | “聚力赋能·共创未来”高校创新创业交流暨项目路演活动(主办:园区;协办:滇池学院、昆明城市学院、五华区大学生创业联盟) |\n| 2024-05-10 | 昆明城市学院师生赴园区“取经”参观交流 |\n| 2024-05-12 | 春城晚报、", "2024年5月园区办了哪些重要活动?", "春城晚报和云南日报啥时候整版报道了园区?标题叫啥?", "‘聚力赋能·共创未来’这个高校创业活动是谁主办的?有哪些学校参与?", "昆明城市学院师生啥时候来园区参观交流的?", "青年企业家协会啥时候走进园区揭牌创新创业基地的?", "云南日报整版深度报道《创业有园 未来可期》 |\n| 2024-05-13 | “昆明市青年企业家协会走进园区暨创新创业基地揭牌仪式” |\n| 2024-05-14 | “云南高校·昆明市大学生创业园”就业创业交流活动暨创新创业基地揭牌仪式(云南经济管理学院、昆明技师学院80余人参加) |\n| 2026-03-13 | 《入驻管理规程》印发(昆明市人力资源和社会保障局) |\n| 2026-03-20 | 2026年第一期入驻申请受理截止:收到申请38个,25个通过初审、", "《入驻管理规程》是哪年哪月印发的?谁发的?", "2026年第一期入驻申请截止后,初审通过了多少个?复审又收了多少个?", "第一期集中评审具体怎么评的?流程是啥?", "2026年第一期最终有多少家企业或项目正式入驻了?", "2026年第二期入驻招募公告是啥时候发布的?", "13个提交复审 |\n| 2026-03-24 | 第一期集中评审(30个路演参评,评审走“资格复核—现场路演—专家提问—综合评议”,量化打分) |\n| 2026-03-25 | 《工作简讯〔2026〕第1期》:第一期最终确定23个优质企业(项目)正式入驻 |\n| 2026-04-15 | 发布2026年第二期入驻招募公告 |\n| 2026-04-24 | “创赢未来”2026创业大赛昆明选拔赛暨马兰花创业培训讲师大赛落幕(主办:昆明市人社局;", "‘创赢未来’2026创业大赛昆明选拔赛是啥时候结束的?谁主办的?", "马兰花创业培训讲师大赛跟园区有关系吗?是不是在园区办的?", "2026年创业大赛昆明选拔赛和讲师大赛是不是同一个活动?", "昆明市人社局主办的这场创业大赛,跟园区日常运营有啥关联?", "‘创赢未来’创业大赛昆明选拔赛落幕那天,园区有没有参与或者协办?", "承办:昆明市公共就业和人才服务中心;4月20日起4天路演,13个项目晋级省级大赛) |\n| 2026-05-06 | 《工作简讯〔2026〕第2期》:此梳理第一期23家入驻企业全覆盖实地摸排,2家长期停滞企业督促整改 |\n| 2026-05-20 | “滇水逐梦·雨林创航”2026昆明·西双版纳创业路演PK赛(西双版纳州公共就业和人才服务中心、滇池学院、园区联合主办;园区企业鬼才明AI活动策划工作室、", "昆明市公共就业和人才服务中心主办的创业路演活动什么时候开始?一共几天?有多少项目进了省赛?", "‘滇水逐梦·雨林创航’2026昆明·西双版纳创业路演PK赛是哪几家单位联合办的?园区里哪家企业参与了活动策划?", "园区最近有没有对入驻企业做实地摸排?摸排了多少家?发现什么问题?怎么处理的?", "云南师范大学和园区搞的就业创业实践基地揭牌仪式,具体有哪些国家的留学生参加了?主题是不是叫‘校地产教融合、跨境双创人才共育’?", "云南旅游职业学院和云南师范大学这两所学校,最近分别什么时候来园区开展就业创业交流活动的?", "云南定影文化传播有限公司协办) |\n| 2026-06-03 | 云南旅游职业学院赴园区开展就业创业交流活动 |\n| 近期(2026) | 云南师范大学与园区联合举办就业创业交流活动暨就业创业实践基地揭牌仪式(开启“校地产教融合、跨境双创人才共育”,缅甸、越南、印尼、老挝等国留学生参加) |\n---", "园区的官方名称全称是什么?在哪些正式文件或设计稿里有明确出处?", "昆明市大学生创业园的定稿文件除了.ai源文件,还提供了什么格式的版本?比如PNG或者JPG?", "园区的官方文件定稿是叫《昆明市大学生创业园 定稿》吗?这个标题出现在哪里?", "园区有没有统一的品牌视觉识别规范?比如Logo、字体、配色这些?", "‘昆明市大学生创业园’这个名称在品牌VI里是怎么写的?英文全称是什么?", "> 出处:昆明市大学生创业园 定稿(.ai 源文件 + 制版PNG/JPG)", "园区Logo是什么样子的?颜色搭配是哪两种?字母K和M是怎么组合的?", "园区的主Logo是橙色和蓝色搭配吗?那个圆环形/动感造型是不是由K和M组成的?", "园区Logo里的英文全称怎么拼?是不是‘KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK’?", "园区VI里最常用的排版方式是怎样的?比如Logo和文字一般怎么摆放?", "园区品牌标识强调橙蓝配色,这个‘橙’和‘蓝’具体指哪种橙、哪种蓝?有没有标准色值?", "14. 品牌VI设计规范\n- **Logo**:橙、蓝双色组合标识——橙色“K”与深蓝“M”交织成圆环形/动感造型,英文全称“KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK”,主推版式居中,品牌色橙蓝相衬。", "园区品牌VI规定的主色有哪些?橙色和蓝色的具体色值是多少?", "VI规范里说的品牌主色,是不是用设计软件拾色器取的数值?能直接用在PS或AI里吗?", "园区橙色和蓝色的标准色号分别是多少?CMYK、RGB或者HEX值有公布吗?", "除了橙蓝双色,园区VI里还有没有其他辅助色或禁用色规定?", "品牌主色的取值依据是设计稿里的拾色器读数,那这个‘设计稿’指的是哪个版本的文件?", "14. 品牌VI设计规范\n- **品牌主色(设计稿拾色器取值)**:", "园区Logo的橙蓝双色组合,和VI里定义的品牌主色是不是完全一致?有没有细微差别?", "‘橙色K’和‘深蓝M’交织的Logo造型,在不同尺寸(比如名片vs展板)下会不会有简化版?", "园区对外宣传材料中,Logo必须居中排版吗?有没有允许左对齐或其他变体?", "英文全称‘KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK’在VI里有没有大小写或缩写规范?", "VI规范第14条讲的是品牌设计,那前面13条都包括哪些内容?比如字体、图标、应用延展这些?", "| 色彩 | Hex | R | G | B | 说明 |\n|---|---|---|---|---|---|\n| 主色·橙 | `#FC8318` | 252 | 131 | 24 | 活力橙,主标识/强调 |\n| 辅色·深蓝 | `#022B97` | 2 | 43 | 151 | 深邃蓝,主标识/正文 |", "园区的主色调橙色和辅助色深蓝色的十六进制代码分别是多少?", "活力橙和深邃蓝对应的RGB数值各是多少?", "品牌VI里用的主色和辅色具体叫什么名字?分别用在哪些地方?", "橙色#FC8318和蓝色#022B97哪个是主色哪个是辅色?它们各自代表什么含义?", "园区LOGO和正文文字分别用的是哪种颜色?主色橙和辅色蓝的具体用途有哪些?", "14. 品牌VI设计规范\n- **配套文件**:`昆明市大学生创业园 定稿.ai`(矢量源文件)、`_画板 1.png/.jpg`(主画板成品)、`_画板 1 副本.png`(色彩规范参考)。", "昆明市大学生创业园的品牌VI源文件叫什么名字?在哪里能拿到?", "‘昆明市大学生创业园 定稿.ai’这个AI文件是做什么用的?", "主画板成品图和色彩规范参考图的文件名分别是什么?格式是png还是jpg?", "VI设计规范里提到的三个配套文件具体指哪几个?各自用途是什么?", "想改LOGO或者做宣传物料,该找哪个源文件?有没有成品图和配色参考图?", "### 附注:数据口径说明", "绩效数据里的‘认定以来累计’‘本年度’‘当前实有’分别是什么意思?", "为什么本月报表说累计出园149户,省级申报却写150户?差这1户是怎么回事?", "不同表格中数据口径不一致时,应该以哪个为准?", "‘填报时点不同’具体指什么?会影响数据准确性吗?", "数据口径说明里提到的‘照原文保留’是什么意思?是不是不能自行修改差异?", "14. 品牌VI设计规范\n1. **累计数与当年数**:第2节绩效、第3节月报表中均区分“认定以来累计 / 本年度 / 当前实有”。本月报表中累计出园149户与省级申报累计出园150户相差1户,为两表填报时点不同所致,均照原文保留。", "绩效表里‘累计数’和‘当年数’怎么区分?分别对应哪些章节?", "第2节绩效和第3节月报表的数据分类方式一样吗?", "‘认定以来累计’‘本年度’‘当前实有’这三个统计维度在报表里怎么体现?", "园区绩效数据的统计逻辑是按自然年、财政年还是认定周期?", "为什么同一指标(比如出园户数)在不同表格里数值不一样?背后的数据逻辑是什么?", "14. 品牌VI设计规范\n2. **入驻规模**:信息表(39家)面向内部全量项目;省级申报名单(31家)为认定口径,项目名称个别有别。", "园区现在一共入驻了多少个项目?内部统计和省级申报为什么数量不一样?", "信息表列了39家,省级申报只列31家,少了8家是没通过认定吗?", "内部全量项目名单和省级认定名单的项目名称为啥有的不一样?是改名了吗?", "39家和31家这两个数字分别代表什么范围?是否包含孵化中和已出园的项目?", "项目名称在不同名单里有出入,是命名规范不统一,还是认定标准导致的差异?", "14. 品牌VI设计规范\n3. **宣传册数据**(媒体报道/外宣口径)与官方申报累计口径存在差异,引用时务必注明出处(见第12节提示)。", "宣传册里写的媒体报道数据和官方申报的累计数据不一样,该怎么引用才规范?", "外宣口径和官方申报口径的数据对不上,引用的时候要注意什么?", "宣传册里的媒体报道数据跟别的地方不一样,引用时要怎么注明来源?", "第12节提示里说的引用出处具体指什么?", "14. 品牌VI设计规范\n4. 本文件所列数值均直接源自原始文件,未作推算加工。", "文件里的数值是直接抄的原始资料,还是自己算出来的?", "这些数字是原始数据还是加工过的?", "文档里的数值有没有经过推算或调整?", "所有数据都是直接从原始文件拿的,没改过吧?", "---\n*文档生成日期:2026年 · 依据 `.xlsx / .xls / .wps / .pdf / .png` 全量原始资料整理*", "这份文档是哪年生成的?", "文档生成日期写的是2026年,是未来时间吗?是不是填错了?", "整理这份文档用的原始文件格式有哪些?", "文档说是根据Excel、PDF这些原始资料整理的,具体支持哪些格式?"], "embed_owner": [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 15, 15, 15, 15, 15, 15, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 19, 19, 19, 19, 19, 19, 20, 20, 20, 20, 20, 20, 21, 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 23, 23, 23, 23, 23, 23, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 26, 26, 26, 26, 26, 26, 27, 27, 27, 27, 27, 27, 28, 28, 28, 28, 28, 28, 29, 29, 29, 29, 29, 29, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 43, 43, 43, 43, 43, 43, 44, 44, 44, 44, 44, 44, 45, 45, 45, 45, 45, 45, 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 48, 48, 49, 49, 49, 49, 49, 49, 50, 50, 50, 50, 50, 50, 51, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 52, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 55, 55, 55, 55, 55, 55, 56, 56, 56, 56, 56, 56, 57, 57, 57, 57, 57, 57, 58, 58, 58, 58, 58, 58, 59, 59, 59, 59, 59, 59, 60, 60, 60, 60, 60, 60, 61, 61, 61, 61, 61, 61, 62, 62, 62, 62, 62, 62, 63, 63, 63, 63, 63, 63, 64, 64, 64, 64, 64, 64, 65, 65, 65, 65, 65, 65, 66, 66, 66, 66, 66, 66, 67, 67, 67, 67, 67, 67, 68, 68, 68, 68, 68, 68, 69, 69, 69, 69, 69, 69, 70, 70, 70, 70, 70, 70, 71, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72, 72, 73, 73, 73, 73, 73, 73, 74, 74, 74, 74, 74, 74, 75, 75, 75, 75, 75, 75, 76, 76, 76, 76, 76, 76, 77, 77, 77, 77, 77, 77, 78, 78, 78, 78, 78, 78, 79, 79, 79, 79, 79, 79, 80, 80, 80, 80, 80, 80, 81, 81, 81, 81, 81, 81, 82, 82, 82, 82, 82, 82, 83, 83, 83, 83, 83, 83, 84, 84, 84, 84, 84, 84, 85, 85, 85, 85, 85, 85, 86, 86, 86, 86, 86, 86, 87, 87, 87, 87, 87, 87, 88, 88, 88, 88, 88, 88, 89, 89, 89, 89, 89, 89, 90, 90, 90, 90, 90, 90, 91, 91, 91, 91, 91, 91, 92, 92, 92, 92, 92, 92, 93, 93, 93, 93, 93, 93, 94, 94, 94, 94, 94, 94, 95, 95, 95, 95, 95, 95, 96, 96, 96, 96, 96, 96, 97, 97, 97, 97, 97, 97, 98, 98, 98, 98, 98, 98, 99, 99, 99, 99, 99, 99, 100, 100, 100, 100, 100, 100, 101, 101, 101, 101, 101, 101, 102, 102, 102, 102, 102, 102, 103, 103, 103, 103, 103, 103, 104, 104, 104, 104, 104, 104, 105, 105, 105, 105, 105, 105, 106, 106, 106, 106, 106, 106, 107, 107, 107, 107, 107, 107, 108, 108, 108, 108, 108, 108, 109, 109, 109, 109, 109, 109, 110, 110, 110, 110, 110, 110, 111, 111, 111, 111, 111, 111, 112, 112, 112, 112, 112, 112, 113, 113, 113, 113, 113, 113, 114, 114, 114, 114, 114, 114, 115, 115, 115, 115, 115, 115, 116, 116, 116, 116, 116, 116, 117, 117, 117, 117, 117, 117, 118, 118, 118, 118, 118, 118, 119, 119, 119, 119, 119, 119, 120, 120, 120, 120, 120, 120, 121, 121, 121, 121, 121, 121, 122, 122, 122, 122, 122, 122, 123, 123, 123, 123, 123, 123, 124, 124, 124, 124, 124, 124, 125, 125, 125, 125, 125, 125, 126, 126, 126, 126, 126, 126, 127, 127, 127, 127, 127, 127, 128, 128, 128, 128, 128, 128, 129, 129, 129, 129, 129, 129, 130, 130, 130, 130, 130, 130, 131, 131, 131, 131, 131, 131, 132, 132, 132, 132, 132, 132, 133, 133, 133, 133, 133, 133, 134, 134, 134, 134, 134, 134, 135, 135, 135, 135, 135, 135, 136, 136, 136, 136, 136, 136, 137, 137, 137, 137, 137, 137, 138, 138, 138, 138, 138, 138, 139, 139, 139, 139, 139, 139, 140, 140, 140, 140, 140, 140, 141, 141, 141, 141, 141, 141, 142, 142, 142, 142, 142, 142, 143, 143, 143, 143, 143, 143, 144, 144, 144, 144, 144, 144, 145, 145, 145, 145, 145, 145, 146, 146, 146, 146, 146, 146, 147, 147, 147, 147, 147, 147, 148, 148, 148, 148, 148, 148, 149, 149, 149, 149, 149, 149, 150, 150, 150, 150, 150, 150, 151, 151, 151, 151, 151, 151, 152, 152, 152, 152, 152, 152, 153, 153, 153, 153, 153, 153, 154, 154, 154, 154, 154, 154, 155, 155, 155, 155, 155, 155, 156, 156, 156, 156, 156, 156, 157, 157, 157, 157, 157, 157, 158, 158, 158, 158, 158, 158, 159, 159, 159, 159, 159, 159, 160, 160, 160, 160, 160, 160, 161, 161, 161, 161, 161, 161, 162, 162, 162, 162, 162, 162, 163, 163, 163, 163, 163, 163, 164, 164, 164, 164, 164, 164, 165, 165, 165, 165, 165, 165, 166, 166, 166, 166, 166, 166, 167, 167, 167, 167, 167, 167, 168, 168, 168, 168, 168, 168, 169, 169, 169, 169, 169, 169, 170, 170, 170, 170, 170, 170, 171, 171, 171, 171, 171, 171, 172, 172, 172, 172, 172, 172, 173, 173, 173, 173, 173, 173, 174, 174, 174, 174, 174, 174, 175, 175, 175, 175, 175, 175, 176, 176, 176, 176, 176, 176, 177, 177, 177, 177, 177, 177, 178, 178, 178, 178, 178, 178, 179, 179, 179, 179, 179, 179, 180, 180, 180, 180, 180, 180, 181, 181, 181, 181, 181, 181, 182, 182, 182, 182, 182, 182, 183, 183, 183, 183, 183, 183, 184, 184, 184, 184, 184, 184, 185, 185, 185, 185, 185, 185, 186, 186, 186, 186, 186, 186, 187, 187, 187, 187, 187, 187, 188, 188, 188, 188, 188, 188, 189, 189, 189, 189, 189, 189, 190, 190, 190, 190, 190, 190, 191, 191, 191, 191, 191, 191, 192, 192, 192, 192, 192, 192, 193, 193, 193, 193, 193, 193, 194, 194, 194, 194, 194, 194, 195, 195, 195, 195, 195, 195, 196, 196, 196, 196, 196, 196, 197, 197, 197, 197, 197, 197, 198, 198, 198, 198, 198, 198, 199, 199, 199, 199, 199, 199, 200, 200, 200, 200, 200, 200, 201, 201, 201, 201, 201, 201, 202, 202, 202, 202, 202, 202, 203, 203, 203, 203, 203, 203, 204, 204, 204, 204, 204, 204, 205, 205, 205, 205, 205, 205, 206, 206, 206, 206, 206, 206, 207, 207, 207, 207, 207, 207, 208, 208, 208, 208, 208, 208, 209, 209, 209, 209, 209, 209, 210, 210, 210, 210, 210, 210, 211, 211, 211, 211, 211, 211, 212, 212, 212, 212, 212, 212, 213, 213, 213, 213, 213, 213, 214, 214, 214, 214, 214, 214, 215, 215, 215, 215, 215, 215, 216, 216, 216, 216, 216, 216, 217, 217, 217, 217, 217, 217, 218, 218, 218, 218, 218, 218, 219, 219, 219, 219, 219, 219, 220, 220, 220, 220, 220, 220, 221, 221, 221, 221, 221, 221, 222, 222, 222, 222, 222, 222, 223, 223, 223, 223, 223, 223, 224, 224, 224, 224, 224, 224, 225, 225, 225, 225, 225, 225, 226, 226, 226, 226, 226, 226, 227, 227, 227, 227, 227, 227, 228, 228, 228, 228, 228, 228, 229, 229, 229, 229, 229, 229, 230, 230, 230, 230, 230, 230, 231, 231, 231, 231, 231, 231, 232, 232, 232, 232, 232, 232, 233, 233, 233, 233, 233, 233, 234, 234, 234, 234, 234, 234, 235, 235, 235, 235, 235, 235, 236, 236, 236, 236, 236, 236, 237, 237, 237, 237, 237, 237, 237, 238, 238, 238, 238, 238, 238, 238, 239, 239, 239, 239, 239, 239, 239, 240, 240, 240, 240, 240, 240, 240, 241, 241, 241, 241, 241, 241, 241, 242, 242, 242, 242, 242, 242, 242, 243, 243, 243, 243, 243, 243, 243, 244, 244, 244, 244, 244, 244, 244, 245, 245, 245, 245, 245, 245, 245, 246, 246, 246, 246, 246, 246, 247, 247, 247, 247, 247, 247, 247, 248, 248, 248, 248, 248, 248, 248, 249, 249, 249, 249, 249, 249, 249, 250, 250, 250, 250, 250, 250, 250, 251, 251, 251, 251, 251, 251, 251, 252, 252, 252, 252, 252, 252, 252, 253, 253, 253, 253, 253, 253, 253, 254, 254, 254, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 256, 256, 256, 256, 256, 256, 256, 257, 257, 257, 257, 257, 257, 257, 258, 258, 258, 258, 258, 258, 258, 259, 259, 259, 259, 259, 259, 259, 260, 260, 260, 260, 260, 260, 260, 261, 261, 261, 261, 261, 261, 261, 262, 262, 262, 262, 262, 262, 262, 263, 263, 263, 263, 263, 263, 264, 264, 264, 264, 264, 264, 264, 265, 265, 265, 265, 265, 265, 265, 266, 266, 266, 266, 266, 266, 266, 267, 267, 267, 267, 267, 267, 267, 268, 268, 268, 268, 268, 268, 268, 269, 269, 269, 269, 269, 269, 269, 270, 270, 270, 270, 270, 270, 271, 271, 271, 271, 271, 271, 272, 272, 272, 272, 272, 272, 272, 273, 273, 273, 273, 273, 273, 274, 274, 274, 274, 274, 274, 275, 275, 275, 275, 275, 275, 276, 276, 276, 276, 276, 276, 276, 277, 277, 277, 277, 277, 277, 278, 278, 278, 278, 278, 278, 279, 279, 279, 279, 279, 279, 279, 280, 280, 280, 280, 280, 280, 281, 281, 281, 281, 281, 281, 282, 282, 282, 282, 282, 282, 282, 283, 283, 283, 283, 283, 283, 284, 284, 284, 284, 284, 284, 285, 285, 285, 285, 285, 285, 286, 286, 286, 286, 286, 286, 287, 287, 287, 287, 287, 287, 288, 288, 288, 288, 288, 288, 288, 289, 289, 289, 289, 289, 289, 290, 290, 290, 290, 290, 290, 291, 291, 291, 291, 291, 291, 291, 292, 292, 292, 292, 292, 292, 293, 293, 293, 293, 293, 293, 294, 294, 294, 294, 294, 294, 294, 295, 295, 295, 295, 295, 295, 296, 296, 296, 296, 296, 296, 297, 297, 297, 297, 297, 297, 298, 298, 298, 298, 298, 298, 299, 299, 299, 299, 299, 299, 300, 300, 300, 300, 300, 300, 301, 301, 301, 301, 301, 301, 302, 302, 302, 302, 302, 302, 303, 303, 303, 303, 303, 303, 304, 304, 304, 304, 304, 304, 305, 305, 305, 305, 305, 305, 306, 306, 306, 306, 306, 306, 307, 307, 307, 307, 307, 307, 308, 308, 308, 308, 308, 308, 309, 309, 309, 309, 309, 309, 310, 310, 310, 310, 310, 310, 311, 311, 311, 311, 311, 311, 312, 312, 312, 312, 312, 312, 313, 313, 313, 313, 313, 313, 314, 314, 314, 314, 314, 314, 315, 315, 315, 315, 315, 315, 316, 316, 316, 316, 316, 316, 317, 317, 317, 317, 317, 317, 318, 318, 318, 318, 318, 318, 319, 319, 319, 319, 319, 319, 320, 320, 320, 320, 320, 320, 321, 321, 321, 321, 321, 321, 322, 322, 322, 322, 322, 322, 323, 323, 323, 323, 323, 323, 324, 324, 324, 324, 324, 324, 325, 325, 325, 325, 325, 325, 326, 326, 326, 326, 326, 326, 327, 327, 327, 327, 327, 327, 328, 328, 328, 328, 328, 328, 329, 329, 329, 329, 329, 329, 330, 330, 330, 330, 330, 330, 331, 331, 331, 331, 331, 331, 332, 332, 332, 332, 332, 332, 332, 333, 333, 333, 333, 333, 333, 334, 334, 334, 334, 334, 334, 335, 335, 335, 335, 335, 335, 336, 336, 336, 336, 336, 336, 337, 337, 337, 337, 337, 337, 338, 338, 338, 338, 338, 338, 339, 339, 339, 339, 339, 339, 340, 340, 340, 340, 340, 341, 341, 341, 341, 341, 341, 342, 342, 342, 342, 342, 342, 343, 343, 343, 343, 343, 343, 344, 344, 344, 344, 344, 344, 345, 345, 345, 345, 345, 345, 346, 346, 346, 346, 346, 346, 347, 347, 347, 347, 347, 347, 348, 348, 348, 348, 348, 348, 349, 349, 349, 349, 349, 349, 350, 350, 350, 350, 350, 350, 351, 351, 351, 351, 351, 351, 352, 352, 352, 352, 352, 352, 353, 353, 353, 353, 353, 353, 354, 354, 354, 354, 354, 354, 355, 355, 355, 355, 355, 355, 356, 356, 356, 356, 356, 356, 356, 357, 357, 357, 357, 357, 357, 358, 358, 358, 358, 358, 358, 359, 359, 359, 359, 359, 359, 360, 360, 360, 360, 360, 360, 361, 361, 361, 361, 361, 361, 361, 362, 362, 362, 362, 362, 362, 363, 363, 363, 363, 363, 363, 364, 364, 364, 364, 364, 364, 365, 365, 365, 365, 365, 365, 366, 366, 366, 366, 366, 366, 367, 367, 367, 367, 367, 367, 368, 368, 368, 368, 368, 368, 369, 369, 369, 369, 369, 369, 370, 370, 370, 370, 370, 370, 371, 371, 371, 371, 371, 371, 372, 372, 372, 372, 372, 372, 373, 373, 373, 373, 373, 373, 374, 374, 374, 374, 374, 374, 375, 375, 375, 375, 375, 375, 376, 376, 376, 376, 376, 376, 377, 377, 377, 377, 377, 377, 378, 378, 378, 378, 378, 378, 379, 379, 379, 379, 379, 379, 380, 380, 380, 380, 380, 381, 381, 381, 381, 381, 382, 382, 382, 382, 382]} \ No newline at end of file diff --git a/backend/knowledge/kb_qa.json b/backend/knowledge/kb_qa.json new file mode 100644 index 0000000..78231ea --- /dev/null +++ b/backend/knowledge/kb_qa.json @@ -0,0 +1 @@ +{"data_hash": "6503b6c923d23ce4772c6fceb3f4cc0eb53db77f", "chunks": ["> 口径说明:以下政策与数据基于公开资料整理,截至 2026 年 8 月。涉及具体补贴金额、名额、申报条件时,以云南省及昆明市最新官方文件为准;本库作为语义检索与回答口径的参考。", "一、什么是 OPC(一人公司)\n- 定义:一人公司(One Person Company, OPC)指只有一个股东的公司,分为一人有限责任公司和一人股份有限公司两类。", "一、什么是 OPC(一人公司)\n- 法律沿革:传统公司法理论认为公司属社团法人、应有 2 人以上股东;我国自 2005 年修订《公司法》起正式引入一人有限责任公司。", "一、什么是 OPC(一人公司)\n- 时代定位:在 AI 时代,一人公司成为低成本、高效率、普惠型的创业形态——创业者仅凭一人之力,借助 AI 工具即可构建覆盖产品研发、市场营销、客户服务与财务管理的完整业务闭环。", "一、什么是 OPC(一人公司)\n- 核心理念:「你负责商业判断,AI 负责高效执行」,实现从\"出卖单次时间\"到\"经营自动化系统\"的跨越。", "一、什么是 OPC(一人公司)\n- 与自由职业的区别:一人公司具备单一股东、有限责任、极简架构等正规企业属性,同时兼具灵活高效、税负更可控等优势。", "一、什么是 OPC(一人公司)\n- 规模:截至 2025 年 6 月,全国一人有限责任公司已超过 1600 万家。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 总特征:准入放宽与责任强化并存。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 允许设立一人股份有限公司:设立股份有限公司应有一人以上二百人以下发起人,一名股东也可设立股份有限公司。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 取消一人股东数量限制:删除旧法\"一个自然人只能投资设立一个一人有限责任公司,且该一人公司不能投资设立新的一人公司\"的规定;一个自然人可设多个一人公司,可形成投资链条。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 放宽股东类型:删除\"一人公司股东只能是自然人或者法人\"的限制,个体工商户、合伙企业、个人独资企业等理论上也可作股东设立一人公司。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除专节规定:不再采用\"一人有限责任公司\"或\"一人股份有限公司\"的特殊表述,统一为\"只有一个股东的公司\",纳入普通公司规制。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除年度审计强制要求:不再强制一人公司每年编制财务会计报告并审计。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 删除登记特别注明:不再要求在登记及营业执照中注明\"自然人独资\"或\"法人独资\"。", "(责任强化,需重点注意)", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 举证责任倒置:新《公司法》第 23 条第 3 款规定,只有一个股东的公司,股东不能证明公司财产独立于股东自己财产的,应对公司债务承担连带责任。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 财产混同是主要认定依据:司法实践中,公司账户与股东个人账户资金往来频繁是财产混同的主要认定依据。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- AI 时代\"数字资产混同\"风险:用个人名义注册付费订阅 AI 大模型、购买云端算力、将训练数据与生成物存于个人账号,一旦发生纠纷,可能被认定公司缺乏独立财产基础,判令股东承担连带责任。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 不设股东会时须书面决定:股东就利润分配、亏损弥补、修改章程等事项,须以书面形式决定,签名或盖章后置备于公司。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 建立健全财务管理制度,账目清晰、独立核算,严格区分公司与个人财产界限。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 适时出具连续完整的会计年度审计报告,或股东与公司往来专项审计报告,作为财产独立性的有力证据。", "二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行)\n- 将合规视野从\"财务隔离\"拓展到\"数据与数字资产隔离\"。", "三、OPC 创业思路与经验\n- 先完成,再完美:AI 已把门槛拆掉,不要等学会所有技能再开始。", "三、OPC 创业思路与经验\n- 最小成本验证模式:用 AI 思考与决策,用外包解决人力,用最小成本验证商业假设。", "三、OPC 创业思路与经验\n- 聚焦四力:产品能力(解决什么问题)、流量能力(客户如何找到你)、交付能力(如何高效完成服务)、变现能力(如何设计商业模式)。", "(盈利三把钥匙与商业模式)", "三、OPC 创业思路与经验\n- 盈利三把钥匙:用 AI 降本增效、用信任创造溢价、用平台借势腾飞。", "三、OPC 创业思路与经验\n- 六大主流商业模式:知识付费(课程/咨询/私教)、内容变现(公众号/小红书/抖音)、服务外包(代运营/代写/代设计)、工具产品(模板/插件/SaaS)、社群运营(付费社群/会员制)、混合模式。", "三、OPC 创业思路与经验\n- 从盘点个人技能 → 验证市场需求 → 找准差异化定位 → 用极低成本测试 MVP → 搭建自动化内容引流矩阵与私域池 → 打造标准化交付质控系统 → 设计高溢价产品矩阵 → 构建抗风险的多元化收入线。", "三、OPC 创业思路与经验\n- 电商:河南创业者卧床 70 天,用 AI 替代原计划的 10 人团队做跨境电商,开出 12 家网店(后优化至 4 家主力店),单店月销 5-8 万、月净利 2.5-3 万,每天只需工作 2-3 小时;用扣子搭工作流自动生成经营日报。", "三、OPC 创业思路与经验\n- 内容与服务:无锡创业者 2025 年转型\"企业陪跑\",帮企业做小红书内容转化;指出 OPC 两大难题是\"如何定价\"与\"如何建立交易机制(个人即产品、前期缺乏标准化交付物)\"。", "三、OPC 创业思路与经验\n- 全 AI 员工公司:创业者用开源 AI 代理框架创建\"零人类公司\",业务含卖教程、AI 技能市场抽佣、企业定制代理,几周收入近 20 万美元,运营成本仅约每月 1500 美元。", "三、OPC 创业思路与经验\n- 大厂裸辞:35 岁管理者裸辞环游多国,靠 AI 创立一人公司,兼任大模型工具开发者、企业咨询顾问、自媒体博主,年入近百万。", "(清醒认知与挑战)", "三、OPC 创业思路与经验\n- AI 没有消灭赚钱的门槛:认知、决策能力甚至运气同样重要;没有商业逻辑,再好的工具也没用。", "三、OPC 创业思路与经验\n- 理性看待收入神话:网上\"一人公司月入十万\"的故事大多不靠谱,成功相当程度依赖运气。", "三、OPC 创业思路与经验\n- 一人公司的孤独感与亲力亲为:现金流、回款、定价等都需要自己承担。", "(省级专项:2026 年 7 月云南省人社厅《关于支持服务人工智能 OPC 创业的若干措施》)", "四、云南当前生效的 OPC 超级个体政策\n- 创业生态社区:鼓励盘活闲置楼宇、高校实训场地等,整合算力、绿电、数据、市场渠道等要素,打造\"低成本空间+普惠算力+全链条生态服务\"的 AI OPC 创业生态社区;运营效果好的可按创业孵化载体补助政策认定补助。", "四、云南当前生效的 OPC 超级个体政策\n- 免费工位与安居支持:支持现有创业园区开设 OPC 专区,提供拎包入驻免费工位;有条件地区为 OPC 创业人才提供免租或低租人才公寓、青年驿站。", "四、云南当前生效的 OPC 超级个体政策\n- 金融支持:\"云岭创业贷\"——企业法人最高可申请 500 万元信用贷款、3000 万元抵押担保贷款;个人创业者最高可申请 50 万元信用贷款、1000 万元抵押担保贷款;鼓励推出\"算力贷\"\"数智创新专项信贷\"等专属产品。", "四、云南当前生效的 OPC 超级个体政策\n- 创业补贴:符合条件者给予 5000 元一次性创业补贴;电商创业项目每个最高补贴 3 万元。", "四、云南当前生效的 OPC 超级个体政策\n- 见习补贴:吸纳高校毕业生就业见习的按每人每月 1500 元(留用率达 50% 以上可增至 2000 元)发放见习补贴。", "四、云南当前生效的 OPC 超级个体政策\n- 培训补贴:AI 相关职业(如数据标注员)纳入培训补贴范围,最高可获 1700 元创业培训补贴和 7280 元技能培训补贴。", "四、云南当前生效的 OPC 超级个体政策\n- 人才政策:符合条件的 AI OPC 创始人纳入\"兴滇英才支持计划\"等省级人才计划;设立人工智能专业职称评审;打破学历、年龄、资历限制建立人才自主评价机制。", "四、云南当前生效的 OPC 超级个体政策\n- 政务服务:深化\"个人创业一件事\"集成办理,实现 OPC 企业登记一表申请、一网联办、一次办结。", "(市级:昆明市大学生创业园)", "四、云南当前生效的 OPC 超级个体政策\n- 面向对象:在校大学生、毕业不超过 8 年的高校毕业生、归国及外籍留学生等青年创业者。", "四、云南当前生效的 OPC 超级个体政策\n- 入驻方向:项目须聚焦昆明市\"10+N\"产业链(人工智能、数字经济、电子信息、新媒体直播、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等);获市级以上创业大赛奖项、有突出学术成就或高新专利者优先。", "四、云南当前生效的 OPC 超级个体政策\n- 空间保障:免费提供办公工位或独立区域。", "四、云南当前生效的 OPC 超级个体政策\n- 孵化服务:专家一对一创业指导、商业模式梳理、沙龙路演。", "四、云南当前生效的 OPC 超级个体政策\n- 融资支持:联动创投机构设立种子资金,每年组织不少于 15 场融资对接活动。", "四、云南当前生效的 OPC 超级个体政策\n- 政策对接:协助申请创业扶持资金、税收减免、社保补贴。", "四、云南当前生效的 OPC 超级个体政策\n- 资源整合:对接高校、科研院所、龙头企业。", "四、云南当前生效的 OPC 超级个体政策\n- 专项赋能:免费提供\"AI 育笋计划\"和\"AI+OPC\"全套培训(涵盖 AI 应用、一人公司落地、成本控制等)。", "四、云南当前生效的 OPC 超级个体政策\n- 综合服务:工商注册、税务代办、知识产权咨询、法律援助一站式支持。", "(OPC 核心范式)", "五、OPC 的几种创业模式\n- OPC 的核心是「1 个创始人 + N 个 AI 数字员工」的 1+N 模式:创始人负责战略决策、创意设计与关键价值判断,AI 承担代码生成、内容创作、数据运营、客户服务等标准化重复工作。", "五、OPC 的几种创业模式\n- OPC 将创业从「资本密集型」转向「认知密集型」:无需融资和组建团队,只需对领域有深度认知,再用 AI 将其转化为产品、服务或收入。", "五、OPC 的几种创业模式\n- 创业门槛从「资源门槛」降为「认知门槛」:代码可用 Claude Code 写、内容可用 AI 生成、跨语言可实时翻译、客户对接可用 AI Agent 筛选。", "(六大已跑通的 OPC 商业模式)", "五、OPC 的几种创业模式\n- 专业服务型:以深度专业技能为核心价值,交付定制化解决方案或咨询,按项目或小时收费,利润率高。", "五、OPC 的几种创业模式\n- 高端服务型:以行业认知与战略判断力为核心,提供高端顾问、私教、战略规划,高客单(一单可达几十万)。", "五、OPC 的几种创业模式\n- 知识产品型:将系统化专业知识做成标准化数字产品(课程、电子书、会员),边际交付成本趋近于零,最被看好。", "五、OPC 的几种创业模式\n- 数字产品型:可复用的软件或数字工具(SaaS、应用、插件、模板),订阅制或一次性/API 收费,规模化潜力最大。", "五、OPC 的几种创业模式\n- 创意内容型:靠个人影响力或 IP,产出博客、视频、社媒内容,通过广告分成、品牌合作、知识付费变现。", "五、OPC 的几种创业模式\n- 平台整合型:做资源连接与撮合,自身不生产产品,靠佣金或撮合费。", "(如何选择适合你的模式)", "五、OPC 的几种创业模式\n- 先想清三个问题:卖「能力」还是卖「系统」?服务边界清不清楚?你是「执行者」还是「系统设计师」?", "五、OPC 的几种创业模式\n- 顾问/咨询型适合专业壁垒深厚、客户关系强的人;AI 可解放线索收集、标准问答、方案初稿。", "五、OPC 的几种创业模式\n- 内容/知识型放大个人 IP,内容可标准化复用、多平台分发,边际成本趋零,适合有表达与知识结构化能力的人。", "五、OPC 的几种创业模式\n- 产品化服务型(SaaS/数字产品)将方法论打包成模板或工具,从「卖服务」转向「卖产品」,长期复利最可观但技术含量最高。", "五、OPC 的几种创业模式\n- 热门赛道:健康/医疗/心理支持、硬件/IoT/机器人、教育/培训/知识服务、金融/法律/政务等专业服务、企业服务/SaaS。", "六、如何成立一家 OPC(一人公司)\n- 先副业后主体:不建议立即辞职,先以副业验证模式。", "六、如何成立一家 OPC(一人公司)\n- 个体工商户注册快、税负低,适合早期验证、接小单;有限责任公司信用强、利于 B 端,适合长期发展,但务必公私分明避免财产混同。", "六、如何成立一家 OPC(一人公司)\n- 若业务面向 B 端长期合作选有限公司,早期验证可选个体户。", "六、如何成立一家 OPC(一人公司)\n- 公司名称核准:提交 3-5 个备选名称(行政区划+字号+行业+有限公司),经市场监管局官网或「一网通办/一窗通」平台核准。", "六、如何成立一家 OPC(一人公司)\n- 提交设立登记申请:线上提交公司章程、股东身份证明、注册地址证明等,约 3-5 个工作日领取营业执照。", "六、如何成立一家 OPC(一人公司)\n- 刻制印章:公章、财务章、法人章,到公安备案刻章点办理,费用约 500 元。", "六、如何成立一家 OPC(一人公司)\n- 银行开户:预约开基本户,需带执照、公章、法人身份证,年费约 500-1000 元。", "六、如何成立一家 OPC(一人公司)\n- 税务登记:领取执照后 30 日内办理,核定税种、申请发票。", "六、如何成立一家 OPC(一人公司)\n- 新《公司法》允许单人担任法人且无需监事。", "(注册资本与地址)", "六、如何成立一家 OPC(一人公司)\n- 实行认缴制,注册时无需实缴全部资金;但须在章程明确认缴期限,且 5 年内实缴到位。", "六、如何成立一家 OPC(一人公司)\n- 认缴≠不缴:切忌盲目追高注册资本,经营不善时股东需在认缴范围内承担责任。", "六、如何成立一家 OPC(一人公司)\n- 注册地址须为商用地址(写字楼、商铺、共享办公工位、集中登记地/虚拟地址等),住宅原则上不可用。", "六、如何成立一家 OPC(一人公司)\n- 可租用园区或共享工位,或用集中登记地/虚拟地址(部分园区免费或年费约 3000-6000 元)。", "(记账报税与合规)", "六、如何成立一家 OPC(一人公司)\n- 公司必须建立账簿,即使零收入也须按月或按季申报(零申报),逾期可能被列入经营异常名录。", "六、如何成立一家 OPC(一人公司)\n- 小规模纳税人季度开票≤30 万免增值税(普票);小微企业年应纳税所得额≤300 万按 5% 缴企业所得税(2027 年底前)。", "六、如何成立一家 OPC(一人公司)\n- 一人公司利润分配后转账需缴 20% 分红个税。", "六、如何成立一家 OPC(一人公司)\n- 代理记账费用约 200-500 元/月,起步阶段建议找代账公司。", "六、如何成立一家 OPC(一人公司)\n- 每年 6 月 30 日前登录「国家企业信用信息公示系统」提交年报。", "六、如何成立一家 OPC(一人公司)\n- 严格区分公司财产与股东个人财产,避免公私账户混同,否则股东可能对公司债务承担连带责任。", "七、如何选用合适的 AI 工具\n- 按「场景适配」选工具,不要盲目追新:没有哪一款模型在所有任务上全面领先。", "七、如何选用合适的 AI 工具\n- OPC 中 AI 首先承担软件开发角色(多数团队用 AI 编程),其次才是内容创作。", "七、如何选用合适的 AI 工具\n- 优先选生态开放、能接 API/插件、支持工作流编排的平台,方便搭建自动化。", "七、如何选用合适的 AI 工具\n- 写代码、推理、性价比选 DeepSeek(开源、API 定价低)或智谱 GLM(编程强、可私有化)。", "七、如何选用合适的 AI 工具\n- 中文通用与云生态选通义千问(阿里云百炼 MaaS、长上下文)。", "七、如何选用合适的 AI 工具\n- 多模态、免费 C 端选豆包(字节),创意内容可用其视频/图像生成。", "七、如何选用合适的 AI 工具\n- 长文档、研报处理选 Kimi(超长上下文)。", "七、如何选用合适的 AI 工具\n- 政企、资质、央国企场景选文心一言(百度,合规资质全)或讯飞星火(语音、信创)。", "七、如何选用合适的 AI 工具\n- 微信生态、快慢思考选腾讯混元。", "(Agent 与工作流搭建平台)", "七、如何选用合适的 AI 工具\n- 扣子(Coze):字节推出的零代码 Agent 平台,拖拽式搭建,适合个人/非技术用户,支持豆包、DeepSeek、Kimi、通义千问、GLM 等模型。", "七、如何选用合适的 AI 工具\n- Dify:企业级、技术向,工作流节点丰富,支持国内外主流模型与私有化部署。", "七、如何选用合适的 AI 工具\n- 用无代码平台可把客服、内容、竞品、线索、数据等搭建为智能体,月成本可低至约 170 元。", "八、AI Agent(智能体)概念与一人公司应用\n- AI 智能体是一套能自主理解目标、制定计划、执行操作、自我修正的完整闭环系统,不同于普通聊天机器人。", "八、AI Agent(智能体)概念与一人公司应用\n- 核心公式:AI Agent = 大模型 + 记忆 + 主动规划 + 工具使用。", "八、AI Agent(智能体)概念与一人公司应用\n- 三大特征:自主决策(自己判断下一步)、工具调用(操作浏览器、调用 API、读写文件、发邮件)、持续学习(优化策略)。", "八、AI Agent(智能体)概念与一人公司应用\n- 成熟智能体通常含五个核心模块:大语言模型(大脑)、任务规划、记忆存储、工具调用、环境感知。", "(如何搭建一个智能体)", "八、AI Agent(智能体)概念与一人公司应用\n- 角色设定:赋予具体人员设置与知识边界,上传历史文档。", "八、AI Agent(智能体)概念与一人公司应用\n- 知识库挂载(RAG):整理笔记、业务 SOP、白皮书,通过文档切片与向量检索让 AI 具备「记忆」。", "八、AI Agent(智能体)概念与一人公司应用\n- 工作流设计:将重复劳动逻辑化,如自媒体运营分身:聚焦热点、生成文案、生成图表、多平台分发。", "八、AI Agent(智能体)概念与一人公司应用\n- 插件与 API 集成:接入搜索、数据库、API 插件,让智能体具备「手脚」。", "八、AI Agent(智能体)概念与一人公司应用\n- 避坑:防止幻觉(强制优先从知识库取答案)、遵循 MVP 原则不过度设计、定期分析对话日志迭代提示词。", "(OPC 的 1+N 组织架构)", "八、AI Agent(智能体)概念与一人公司应用\n- 核心层(1 人)只做四件事:定战略、定标准、审关键输出、管现金流。", "八、AI Agent(智能体)概念与一人公司应用\n- AI 智能体层(N 个数字员工):按岗位拆分为独立 Agent,如产品/研发、内容/营销、流量/投放、销售/客服、交付/项目、财务/行政。", "八、AI Agent(智能体)概念与一人公司应用\n- 外部协作层:把设计、剪辑、翻译等 AI 搞不定的重活外包。", "(流水线工作流与运营闭环)", "八、AI Agent(智能体)概念与一人公司应用\n- 标准工作流:需求输入、AI 拆解、AI 生成初稿、创始人审核微调、AI 优化排版、成品入库或上线。", "八、AI Agent(智能体)概念与一人公司应用\n- 运营闭环:AI 批量产内容并多平台分发、AI 客服 7×24 响应并自动生成报价单、标准化 SOP 加 AI 自动交付、AI 记账报税。", "八、AI Agent(智能体)概念与一人公司应用\n- 效果:启动成本可从传统创业的 20 万-100 万压缩到每月 500-5000 元;试错周期从半年缩短到 2-6 周。", "九、国内主要大模型厂商与选型\n- 国内大模型市场呈「三超多强」格局:第一梯队为豆包(字节)、通义千问(阿里)、DeepSeek;第二梯队为智谱 AI、文心一言(百度)、混元(腾讯);第三梯队为 Kimi、MiniMax 等创新企业。", "九、国内主要大模型厂商与选型\n- 在简单问答、常识领域各大模型差异甚小,差异主要体现在复杂指令、多模态理解生成与多轮对话。", "九、国内主要大模型厂商与选型\n- DeepSeek:推理、代码、数学、性价比强,开源(MIT),API 定价低,适合写代码与高性价比推理。", "九、国内主要大模型厂商与选型\n- 通义千问(阿里):通用、中文、企业云生态强,阿里云百炼 MaaS,长上下文,回答完整度突出。", "九、国内主要大模型厂商与选型\n- 豆包(字节):C 端应用、多模态、Agent,日活高、免费多模态,深度逻辑与代码能力相对弱。", "九、国内主要大模型厂商与选型\n- 智谱 GLM:编程、长程 Agent 任务强,开源、长上下文,政企私有化根基扎实。", "九、国内主要大模型厂商与选型\n- 文心一言(百度):全模态、搜索、中文写作,功能完善度领先、合规资质最全。", "九、国内主要大模型厂商与选型\n- Kimi(月之暗面):超长文档、研报解析领先。", "九、国内主要大模型厂商与选型\n- 讯飞星火(科大讯飞):中文、语音交互、教育医疗,央国企与信创环境应用广。", "九、国内主要大模型厂商与选型\n- 混元(腾讯):社交、多模态、快慢思考,微信生态深度整合。", "九、国内主要大模型厂商与选型\n- 写代码商用:智谱 GLM 或 DeepSeek;长文档处理:Kimi;多模态/免费 C 端:豆包;企业政企落地:智谱、文心一言或通义千问。", "九、国内主要大模型厂商与选型\n- 各家价格口径不统一(有美元/人民币计、缓存命中与上下文位影响单价),比价务必回官网核对同一口径。", "九、国内主要大模型厂商与选型\n- 首期成效:受理 58 个项目,23 个成功入驻,覆盖人工智能、低空经济、跨境服务等领域。", "> 数据口径声明:本知识库内容全部依据《昆明市创业园宣传册》《昆明市大学生创业园入驻企业信息表》《昆明市大学生创业园运行情况统计表(2026年7月)》《附件1.省级创业孵化载体相关情况简介材料》《昆明市大学生创业园创业企业(项目)入驻管理规程》等标准资料整理;涉及运营数据时严格区分「官方统计口径」「省级绩效累计口径」「AI 平台运行指标」三类口径,不得混用。", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 全称:昆明市大学生创业园(KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK)", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 性质:云南省首家由政府主办、免费为创业青年大学生提供创业孵化服务的开放式园区", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 成立时间:2009 年(2009 年 9 月成立)", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 主管部门:昆明市人力资源和社会保障局", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 园区地址:昆明市民航路 229 号(昆明市人力资源中心 1—2 楼)", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 建筑面积:约 3000 平方米,可容纳约 50 个大学生创业团队入驻", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 主导产业:数字经济与电子信息产业", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 认定省级创业园时间:2012 年", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 咨询电话:18087174891 / 17787129131 / 18214573650;接收邮箱:714066050@qq.com", "一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》)\n- 服务模式:以「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体全链条创业扶持体系,打造「一站式」创业孵化服务平台", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 实际入驻企业数:38 家(2026 年 7 月报送当月数)", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 园区可容纳企业(实体)个数:49 个", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 开园以来累计投入园区运营资金:560 万元", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计带动就业人数(当年累计):213 人", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计生产经营总额(当年累计):252.15 万元", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 入驻企业累计上缴税利总额(当年累计):1.03 万元", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 开园以来累计成功孵化出园企业数:149 家", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 当年开展活动场次:11 场;当年培训人数:362 人", "二、园区官方运营数据(口径:《2026年7月运行情况统计表》)\n- 备注:统计表 3、9 栏为开园以来累计数,5 栏为报送当月实际入驻数,其余为当年累计数", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 认定为省级创业孵化载体以来累计孵化企业:181 家;当前实有在孵创业实体:31 家", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 团队(实体)出园数:累计 150 家(本年度 11 家)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队发明专利数:累计 171 项(本年度 13 项)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队累计经营收入:21,911.14 万元(约 2.19 亿元)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 入驻团队累计税收缴纳:426.2 万元", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 累计带动就业:1,529 人(本年度 106 人)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 创业指导专家团队:50 人;运营团队:3 人", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 开展创业指导服务活动:累计 83 场(本年度 18 场)", "三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月)\n- 近 3 年入孵实体孵化成功率:95.91%", "四、入驻对象与条件(依据《入驻管理规程》)\n- 入驻对象范围:在校大学生及毕业 8 年内的高校毕业生、归国留学生、外籍留学生等青年创业企业(项目);申请人须担任企业法定代表人或项目负责人;对符合昆明市产业发展的优质创业企业(项目)或 OPC 企业可适当放宽入驻条件", "四、入驻对象与条件(依据《入驻管理规程》)\n- 产业方向:符合国家产业政策,聚焦昆明市「10+N」产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等)", "四、入驻对象与条件(依据《入驻管理规程》)\n- 其他要求:有明确可行的经营计划、启动资金和风险承担能力,具备规范的企业管理制度;无知识产权纠纷、无违法犯罪记录、无经营异常记录", "四、入驻对象与条件(依据《入驻管理规程》)\n- 优先入驻情形:获得市级及以上创业大赛奖项、拥有突出学术成就、重大发明创造及高新专利技术的项目", "园区原则上每年组织两次入驻工作,可适时增减调整。流程如下:", "五、入驻流程(依据《入驻管理规程》)\n1. 提出申请:提交《入驻申请表》《创业计划书》、创业者身份证(外籍提供护照、签证)、学历证明、企业营业执照(已注册企业)、相关资质专利证书等材料", "五、入驻流程(依据《入驻管理规程》)\n2. 项目初审:服务机构对申请材料初审,材料不齐的一次性告知补全", "五、入驻流程(依据《入驻管理规程》)\n3. 入驻评审:从《昆明市就业创业导师库》抽取 3 名评审专家,采取路演答辩方式评审,实行量化打分制(总分 100 分,80 分及以上准予通过)。评分维度:团队资质与能力(20 分)、项目创新性与技术壁垒(25 分)、产业适配与市场前景(25 分)、运营规划与落地能力(15 分)、社会价值与发展潜力(15 分)", "五、入驻流程(依据《入驻管理规程》)\n4. 评审公示:报主管部门备案后,在昆明市人力资源和社会保障局官方网站公示,公示无异议的准予入驻", "五、入驻流程(依据《入驻管理规程》)\n5. 签署协议:签订《入驻孵化协议》,分配办公场地/工位,正式入驻", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- OPC 创业空间:面向以人工智能技术为支撑、聚焦数字经济/AI 应用/软件开发/文创设计/电商直播等轻资产领域的项目,提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 创业苗圃区:面向创意阶段、未注册企业的大学生创业团队,提供开放式共享工位,孵化周期最长 2 年;成长好的项目经评审可推荐至孵化加速区孵化 1 年", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 孵化加速区:面向已完成工商注册、处于初创期的企业,提供独立办公空间,基础孵化周期 2 年,期满经评估合格可申请延期 1 年,累计不超过 3 年", "六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》)\n- 国际创客区:面向归国留学生、外籍留学生创业项目,提供个性化办公空间及国际化配套服务,孵化周期参照孵化加速区执行", "七、政策与孵化服务(依据《宣传册》)\n- 空间保障:免费提供办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等", "七、政策与孵化服务(依据《宣传册》)\n- 孵化服务:创业指导专家一对一辅导,定期举办创业沙龙、创业讲堂、项目路演等", "七、政策与孵化服务(依据《宣传册》)\n- 融资支持:联动省内外创业投资机构,提供政策性贷款、商业银行贷款、股权融资等推荐指导", "七、政策与孵化服务(依据《宣传册》)\n- 政策对接:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策", "七、政策与孵化服务(依据《宣传册》)\n- 资源整合:对接驻昆高校、科研院所、龙头企业,纳入昆明创业创新服务体系,优先参与「春城创业荟」等品牌活动", "七、政策与孵化服务(依据《宣传册》)\n- 专项赋能培训:定期开展 AI 公益性培训(AI 技术应用、数字内容创作、一人公司创业落地、企业 AI 降本增效)", "七、政策与孵化服务(依据《宣传册》)\n- 其他服务:工商注册、税务代办、知识产权咨询、法律援助等", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 年度考核:量化打分制(总分 100 分),维度为经营发展能力(20)、技术创新能力(25)、合规与履约情况(20)、创业带动成效(20)、发展潜力与孵化成效(15);等级:优秀(80 分及以上)、合格(60—79 分)、不合格(60 分以下)", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 退出类型:成功孵化出园、孵化期满退出、主动申请退出、强制勒令退出", "八、年度考核与退出管理(依据《入驻管理规程》)\n- 强制退出情形:累计 30 个工作日无人到现场办公、连续 7 个工作日场地空置且未报备累计超 3 次、给园区造成重大经济损失或名誉损失、不服从管理情节严重、被吊销执照或破产清算、违法违规经营、私自转租或虚报数据等", "九、OPC 概念(一人公司创业模式)\n- OPC 即 One Person Company「一人公司」创业模式:以人工智能技术为支撑,聚焦数字经济、AI 应用、软件开发、文创设计、电商直播等轻资产领域,一人即可启动运营的创业形态", "九、OPC 概念(一人公司创业模式)\n- 园区 OPC 创业空间为 OPC 创业者提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年;园区定期开展「AI 一人公司实战课」等专项培训", "九、OPC 概念(一人公司创业模式)\n- 云南首个人工智能 OPC 创新人才基地落地昆明(2026 年 3 月公开报道),填补省内个体 AI 创业培育空白", "在园企业以《2026年7月运行情况统计表》官方口径 38 家为准;《入驻企业信息表》收录本期入驻企业(项目)名录 39 家,含云南派音人工智能科技、米勒克尔蓝宝石珠宝产业化、中泰研学合作、云南宸中低空经济、昆明智海银高文化科技、人工智能机器人大模型训练、云南廷秀文旅康养、瀚颖 AI+ 教育咨询、云南大学 AI+ 创业平台、仰光客厅、云南上古绝学文化、千楣之路、中越生物医疗、中越文旅交流、绿态环保科技、联翼航空低空经济、酷享野农 AI 农业、滇缅国际设计、昆明舒诺生物科技、", "达岸教育管理、昆明滇油石化、花仙子园艺肥料、Facebook 越南茶器跨境、研X 同行者网络、鬼才明 AI 创意工作室、朵哈·玫瑰特色产业链、昆明云韵体育、昆明舒护安养老服务、南菌优培食用菌、五华区丽裳文化、云南星瑞航空、云南直播私域平台、昆明屿澈电商、中外青少年研学、星杭商务信息咨询、蓝智科技、云品出滇·纸享万家、云迹轻旅、启元人工智能科技。行业分布(按简介分类):人工智能 7 家、跨境电商 5 家、教育培训 4 家、文旅康养 4 家、文化创意 4 家、生物医药 4 家、", "低空经济 3 家、现代农业 3 家、绿色环保 2 家、企业服务 2 家、新材料 1 家", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- TOKEN 消耗:月均约 200 亿 tokens(折合日均约 6.67 亿 tokens、实时速率约 7,700 t/s)——AI 平台运行锚点测算", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- 工具调用:按平均每次工具调用约 15 万 tokens 折算,与 TOKEN 消耗实时联动", "十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径)\n- 实时运营数据请调用 get_park_overview 工具获取,以工具返回为准", "常见问题\n- 问:如何入驻园区?答:提交申请(入驻申请表、创业计划书、身份证、学历证明等)→ 项目初审 → 入驻评审(导师库抽 3 名专家、路演答辩、80 分通过)→ 评审公示(市人社局官网)→ 签订《入驻孵化协议》、安排场地工位;每年原则上两次。", "常见问题\n- 问:园区有哪些政策扶持?答:空间免费、孵化辅导、融资对接、政策申报、资源整合、AI 专项培训、工商税务法律一站式服务(详见「政策与孵化服务」)。", "常见问题\n- 问:园区有多少家企业、带动多少就业?答:按官方统计(2026 年 7 月)实际入驻 38 家、当年累计带动就业 213 人;省级绩效累计口径(截至 2026 年 5 月)累计带动就业 1,529 人、累计经营收入约 2.19 亿元。请说明所需口径。", "常见问题\n- 问:什么是 OPC?答:一人公司(One Person Company)创业模式,详见「OPC 概念」。", "> 整理自 `数据资料/` 目录全部原始材料:\n> 附件1.省级创业孵化载体相关情况简介材料.xlsx · 昆明市大学生创业园入驻企业信息表.xlsx · 昆明市大学生创业园运行情况统计表(2026年7月).xls · 昆明市大学生创业园创业企业(项目)入驻管理规程.wps · 昆明市创业园宣传册.pdf · 空间布局.png / 详细的布局.png · 昆明市大学生创业园 定稿(品牌设计稿)\n>\n> 本文件尽量保留原始表格、文件中的真实数值与原文字,供对外申报、总结汇报、", "宣传推广等使用。\n---", "目录\n1. [园区基本信息](#1-园区基本信息)", "目录\n2. [运营绩效核心指标(省级申报口径)](#2-运营绩效核心指标省级申报口径)", "目录\n3. [2026年1—7月运行情况月统计](#3-2026年17月运行情况月统计)", "目录\n4. [入驻企业完整名录(39家/信息表)](#4-入驻企业完整名录39家信息表)", "目录\n- [A. 孵化加速区(加速1–10)](#a-孵化加速区)", "目录\n- [B. 国际创客区(国际1–7)](#b-国际创客区)", "目录\n- [C. 成长区(成长1–8)](#c-成长区)", "目录\n- [D. OPC创业空间](#d-opc创业空间)", "目录\n- [E. 一楼直播间](#e-一楼直播间)", "目录\n5. [省级申报·入驻实体名单(31家)](#5-省级申报入驻实体名单31家)", "目录\n6. [企业(项目)入驻管理规程要点](#6-企业项目入驻管理规程要点)", "目录\n7. [空间布局](#7-空间布局)", "目录\n8. [宣传册核心内容(园区简介·服务·政策)](#8-宣传册核心内容园区简介服务政策)", "目录\n9. [入驻条件与入驻流程](#9-入驻条件与入驻流程)", "目录\n10. [孵化周期(四大孵化区域)](#10-孵化周期四大孵化区域)", "目录\n11. [报名方式](#11-报名方式)", "目录\n12. [媒体报道与合作媒体](#12-媒体报道与合作媒体)", "目录\n13. [园区新闻与活动大事记](#13-园区新闻与活动大事记)", "目录\n14. [品牌VI设计规范](#14-品牌vi设计规范)", "| 项目 | 内容 |\n|---|---|\n| 载体名称 | 昆明市大学生创业园 |\n| 载体类别 | 省级创业园(2012年获评) |\n| 载体地址 | 云南省昆明市官渡区民航路229号(昆明市人力资源中心二楼) |\n| 成立时间 | 2009年9月(2026年4月全面优化后“焕新亮相”) |\n| 主管部门 | 昆明市人力资源和社会保障局 |\n| 运营机构 | 云南汇浙青创园区投资有限公司(统一社会信用代码 91530112MA6PWKTUX2) |\n| 申报单位 | 昆明市人力资源和社会保障局 |\n| 申报单位负责人 | 黄斌(联系电话 63362979) |\n| 运营负责人 | 李云鹏(联系电话 18087147411) |\n| 载体特色 | 云南省首家政府主办、", "为创业大学生免费提供创业平台的开放式园区 |\n| 主导产业 | 数字经济与电子信息产业 |\n| 载体占地面积 | 3000 平方米 |\n| 孵化场所面积 | 800 平方米 |\n| 载体资产总值 | 400 万元 |\n| 总投资(开园) | 近 100 万元 |\n| 累计投入园区运营资金 | 560 万元 |\n| 可容纳团队(实体)户数 | 50 户(统计表中某口径为 49 个) |\n| 园区等级/称号 | 国家大学科技园体系、省级创业园等(宣传册口径:国家众创空间、", "国家级科技企业孵化器等称号口径不一,以正式文件为准) |\n**园区简介(300字内口径)**\n昆明市大学生创业园位于昆明市民航路301号昆明人力资源中心二楼,2009年9月成立,总投资近100万元,是云南省首家免费为创业大学生提供创业平台的开放式园区,隶属于昆明市人力资源和社会保障局,由昆明市大学生创业园服务中心承担日常管理服务。园区总面积约3000平方米,可容纳50个大学生创业团队入驻,为入驻团队提供免费办公场地及配套设施,并配套政策咨询、创业规划、创业指导、创业培训、", "导师对接、跟踪服务、资金扶持、法律援助等全维度创业支持服务。园区坚持“高起点、高标准、高水平”办园宗旨,遵循“优质、高效、便捷”服务原则与“规范、完善、提高”管理理念,致力打造企业家培育摇篮与全省示范创业园区。截至2026年5月,园区累计孵化企业181家,其中在孵31家、成功出园149家,累计带动就业1529人、上缴利税426.2万元,38家企业累计获市级创业扶持资金149.25万元。\n---", "> 出处:附件1.2 省级创业孵化载体简介(运营绩效情况)\n| 指标 | 认定以来累计 | 本年度 | 当前实有 |\n|---|---|---|---|\n| 可容纳团队(实体)户数 | — | 50 户 | — |\n| 已入驻团队(实体)户数 | 181 户 | 31 户 | 31 户 |\n| 其中:应届高校毕业生(含离校2年内)参创实体数 | 114 户 | 6 户 | — |\n| 团队(实体)出园数 | 150 户 | 11 户 | — |\n| 返乡农民工参加创办实体数 | 0 户 | 0 户 | — |\n| 入驻团队(实体)发明专利数 | 171 项 | 13 项 | — |\n| 入驻团队(实体)经营收入 | 21911.14 万元 | 161.3 万元 | — |\n| 入驻团队(实体)税收缴纳金额 | 426.2 万元 | 0.22 万元 | — |\n| 入驻团队(实体)获得创业资金扶持金额 | 38 户 | 149.25 万元 | — |\n| 孵化创业实体数 | 181 户 | — | 31 户(在孵) |\n| 带动就业人数 | 1529 人 | 106 人 | — |\n| 运营团队人数 | — | 3 人 | — |\n| 创业指导专家团队人数 | — | 50 人 | — |\n| 开展创业指导服务活动次数 | 83 场 | 18 场 | — |\n| 年末孵化场所利用率 | 90%(0.9) | — | — |\n| 近3年入孵实体孵化成功率 | 95.91% | — | — |\n| 近3年入孵实体到期出园率 | 100%(1) | — | — |\n---", "> 出处:昆明市大学生创业园运行情况统计表(2026年7月).xls —《昆明市创业平台运行情况月统计表》及《年度汇总表》", "### 3.1 2026年7月当月快照\n| 指标 | 数值 |\n|---|---|\n| 园区建筑面积 | 3000 m² |\n| 开园以来累计投入园区运营资金 | 560 万元 |\n| 园区可容纳企业(实体)个数 | 49 个 |\n| 园区实际入驻企业数 | 38 个 |\n| 入驻企业累计带动就业人数 | 213 人 |\n| 入驻企业累计生产经营总额 | 252.15 万元 |\n| 入驻企业累计上缴税利总额 | 1.03 万元 |\n| 开园以来累计成功孵化出园企业(实体)数 | 149 户 |\n| 开展活动场次 | 11 场 |\n| 培训人数 | 362 人 |\n> 备注:报表第3、", "9栏数据为开园以来累计数,第5栏为报送当月实际入驻企业数,其他栏均为当年累计数。", "### 3.2 2026年1—7月逐月变化(年度汇总表)\n| 月份 | 园区建筑面积(m²) | 累计运营资金(万元) | 可容纳个数 | 实际入驻企业数 | 带动就业(人) | 累计生产经营总额(万元) | 累计上缴税利(万元) | 累计出园(户) | 活动场次 | 培训人数 |\n|---|---|---|---|---|---|---|---|---|---|---|\n| 1月 | 700 | 510 | 18 | 15 | 73 | 28.3 | 0.04 | 139 | 0 | 0 |\n| 2月 | 700 | 510 | 18 | 15 | 73 | 50.4 | 0.08 | 139 | 0 | 0 |\n| 3月 | 700 | 560 | 18 | 5 | 73 | 59.1 | 0.09 | 149 | 0 | 0 |\n| 4月 | 3000 | 560 | 49 | 24 | 175 | 69.73 | 0.10 | 149 | 0 | 0 |\n| 5月 | 3000 | 560 | 49 | 31 | 179 | 161.3 | 0.22 | 149 | 5 | 149 |\n| 6月 | 3000 | 560 | 49 | 31 | 186 | 170.4 | 0.35 | 149 | 7 | 206 |\n| 7月 | 3000 | 560 | 49 | 38 | 213 | 252.15 | 1.03 | 149 | 11 | 362 |\n> 数据说明:4月起园区迁入民航路229号新场地,建筑面积由700㎡增至3000㎡、", "可容纳数由18个增至49个,运营资金由510万元增至560万元。截至7月底实际入驻38个,累计带动就业213人。\n**填报信息**:单位负责人 李云鹏 / 填表人 李禹贤 / 联系电话 18087174891 / 邮箱 714066050@qq.com / 填表时间 2026年7月31日\n---", "> 出处:昆明市大学生创业园入驻企业信息表.xlsx(共39个项目,序号1–39,按“场地安排”列分区分布)\n> 分区口径:加速=孵化加速区、国际=国际创客区、成长=成长区、OPC=OPC创业空间、一楼直播间。", "### A. 孵化加速区\n(加速1—加速8、加速10,共9家;表中无“加速9”)", "**加速1|云南派音人工智能科技有限公司**\n- 负责人:王思松,2020年毕业于应急管理大学新闻学专业。\n- 简介:OPC一人公司,2026年3月启动,入驻不足一个月即获北京相关投资支持,2026年4月正式注册成立,吸纳就业4人。核心业务主攻人工智能算法及音频模型应用研究,兼备内容创作、运营管理与技术研发综合能力,专注多模态音频技术研发。自研Pine全系列模型家族,覆盖音频识别、向量嵌入、音效生成、配乐创作及数字内容创意中枢,贯通音频理解至声音合成全链路智能。", "**加速2|米勒克尔蓝宝石珠宝产业化(项目)**\n- 负责人:王昱博,2023年3月毕业于美国加州大学圣克鲁兹分校 Multi-/Interdisciplinary 管理学专业。\n- 简介:2026年3月启动,海归留学生发起,覆盖米勒克尔大尺寸蓝宝石新材料与珠宝产业化全链条(晶体生长、材料精密加工、终端珠宝设计)。与产业链上下游深度协同,专注高品质、高附加值蓝宝石珠宝产品,推动蓝宝石新材料与珠宝产业深度融合。", "**加速3|中泰研学合作(项目)**\n- 负责人:刘泽位,泰国西那瓦大学工商管理博士二年级,昆明市侨联青年委员会副会长,精通英语、熟练泰语。\n- 简介:2026年启动运营,海归在读博士发起,围绕西那瓦大学文理学院管理运营,统筹泰国环球工商职业学院中国区事务。与中泰院校、机构深度合作,提供出国留学、中外合作办学、校企合作、产教融合及企业出海一站式服务。", "**加速4|云南宸中低空经济有限公司**\n- 负责人:王爱玲,云南大学EMBA在读,公司负责人。\n- 简介:2026年2月成立,扎根园区的创新型初创企业,专注低空经济全产业链开发运营,依托园区创业扶持体系与营商环境,快速完成项目落地与团队组建,致力于成为云南低空经济领域新兴力量。", "**加速5|昆明智海银高文化科技有限公司**\n- 负责人:宋芃燃,昆明文理学院审计学专业在读,中共党员,公司合伙人。\n- 简介:2026年4月成立,聚焦数字化财税与企业咨询服务,覆盖传统财税、AI智能财税、海外财税、财务人才培训及企业综合咨询。依托AIGC技术打造AI集成服务平台,提供财税咨询、代理记账、法律合规、企业管理、出海服务等一站式解决方案。", "**加速6|人工智能机器人大模型训练(项目)**\n- 负责人:周凯,2022年毕业于天津大学机械设计及其自动化专业。\n- 简介:2026年4月成立,聚焦人工智能机器人大模型训练,围绕大模型研发与落地提供全流程数据支撑与技术服务,开展数据采集、清洗、标注、模型训练及优化迭代,推动机器人大模型在多场景落地应用。", "**加速7|云南廷秀文旅康养服务(项目)**\n- 负责人:李雨桐,2025年毕业于日本樱美林大学老年社会学硕士。\n- 简介:负责人计划5月归国创业。立足云南“旅居云南”趋势与银发经济增长需求,解决民宿、旅居空间、康养点位在服务标准、适老能力、长期居住支持和运营转化方面的短板,建立面向银发人群的文旅旅居服务支持体系。", "**加速8|瀚颖AI+教育信息咨询(项目)**\n- 负责人:王翰啸,2019年毕业于云南大学滇池学院财务管理专业。\n- 简介:昆明瀚颖“AI+数字教育”项目2026年4月成立,由创业培训服务领域人员发起,融合AI与教育教学体系,打造集课程研发、人才培育、企业赋能、师资共建于一体的全链条数字化教育服务生态。", "**加速10|云南大学AI+联合创业服务平台(项目)**\n- 负责人:顾耘华,云南大学艺术与设计学院艺术设计在读硕士。\n- 简介:云南大学在校研究生、毕业生和创业导师共同发起的AI创业平台。以OPC社区AI人才培养与就业服务为核心,覆盖AI短视频、AI漫剧、AI+PPT制作美化等。已帮助多名大学生成功开展OPC创业;联合云南AIGC产业联盟打造“育笋计划”公益品牌,辐射大学生就创业500人+;王升雅《生态秘境》插画在《人与自然》杂志连载六期;王洋《智绘古海——AI复原寒武纪古生物》获国家级创赛金奖;杨沐晨打造西南城市鸟类多样性生态视觉品牌,承办7项生态展览;王润曦协同云南省委宣传部助力会泽白雾村打造乡村文旅振兴名片。", "### B. 国际创客区\n(国际1—国际7,共7家)", "**国际1|仰光客厅(项目)**\n- 负责人:缅甸籍留学生,毕业于云南大学经济学院。\n- 简介:缅甸留华学生发起的中缅文旅融合项目,以仰光近郊沉浸式文化营地为核心载体,面向中国客群、在缅中资机构及本地青年,集住宿、特色餐饮、文化体验、城市导览于一体,与本地社区、手工艺人合作带动就业与增收,搭建跨文化交流平台。", "**国际2|千楣之路(项目)**\n- 负责人:缅甸籍留学生,现就读于云南大学工商管理学院。\n- 简介:缅甸籍留学生创立,主营缅甸传承两千余年的传统护肤产品檀娜卡(黄香楝树干制成,缅甸已于2025年申报联合国非遗)。打造传统原木套装、便捷粉泥、现代美妆三大产品线,以“文化内容+跨境电商”模式引入中国市场。", "**国际3|联翼航空技术低空经济应用国际化拓展(项目)**\n- 负责人:杨俊蓝,2023年毕业于云南财经大学电子商务专业,瑞士商学院MSC硕士在读。\n- 简介:2026年4月启动,在读硕士发起,立足云南面向东南亚的区位门户优势,开展低空经济业务国际化拓展,在航空通航、人工智能与数字信息、教育培训方向布局,输出无人机应用、低空数据服务、跨境技能培训等内容,推动低空经济产业、人才、服务双向辐射东南亚。", "**国际4|酷享野农AI农业(项目)**\n- 负责人:晏和波,西南林业大学农业工程与信息技术专业在校研究生。\n- 简介:2026年4月启动,西南林业大学在校研究生发起,聚焦智慧农业、科研种植、农旅融合,用AI智能视觉提供农业宣传内容制作服务。与科研机构合作开展高原早熟苹果科研种植,与地方政府共建共享农庄,助力乡村振兴。", "**国际5|滇缅国际设计(项目)**\n- 负责人:张浩鹏,2022年毕业于云南财经职业学院审计学专业。\n- 简介:2026年4月启动,依托一带一路机遇,立足滇缅对外开放节点开展工程设计服务,创新跨国协同设计模式,融入绿色建造技术,覆盖交通、城乡基建等领域,与境外机构合作推动跨境项目落地。", "**国际6|昆明舒诺生物科技有限责任公司**\n- 负责人:舒岐涛,云南林业职业技术学院环境艺术专业在校生。\n- 简介:2026年4月成立,聚焦生物科技与宠物营养领域,围绕精准营养、全生命周期养护、清洁标签开展业务,以科学配方与生物活性技术打造宠物精细化营养产品。", "**国际7|达岸教育管理(云南)有限公司**\n- 负责人:吕琳,2022年毕业于泰国暹罗大学MBA研究生专业。\n- 简介:专注德国双元制教育与德国留学一站式服务(2024年启动),与德国院校合作,提供双元制培养、公立院校申请、德语培训、签证办理、境外陪护等全流程服务,帮助学生实现带薪留学、学历提升与稳定就业。", "### C. 成长区\n(成长1—成长8,共8家)", "**成长1|花仙子园艺肥料(云南)有限公司**\n- 负责人:翁淞耀,2023年毕业于云南师范大学社会体育指导与管理专业。\n- 简介:2025年4月成立,专注家庭园艺肥研发生产,针对产品异味大、品质不稳、使用不便等痛点推出安全无异味、易操作系列产品,已实现规模化生产。采用线上线下融合渠道,服务家庭养花、阳台种植民生需求。", "**成长2|研X——同行者网络(项目)**\n- 负责人:陈品祥,2024年昆明理工大学硕士毕业,兼职任教昆明冶金职业大学。\n- 简介:面向全国1621万硕博及6000万泛科研人群的一站式学术成长与资源共享平台。首创“1+6+N”生态(1个实名制平台+文献工具导航、代码共享交易、就业创业对接、学术/情感社交婚恋、科教娱乐、研究生严选商城6大模块)。已完成安卓、iOS、Windows三端开发并上架,已有全国300余所高校注册,两位高校教授以个人资金投入种子轮。", "**成长3|朵哈·玫瑰特色产业链(项目)**\n- 负责人:刘志贤,云南轻纺职业学院艺术设计专业大二在校生。\n- 简介:2026年4月启动,云南轻纺学院在校大学生发起,立足昆明安宁八街核心产区,以地标产品重瓣红玫瑰为核心原料,主打玫瑰米酿特色饮品,与省内农业产业化龙头企业合作实现原料优选、标准化生产与品质管控。", "**成长4|昆明舒护安养老服务有限责任公司**\n- 负责人:严宇佳,2019年毕业于云南艺术学院广播电视编导专业。\n- 简介:OPC一人公司,入驻园区孵化仅一个月成功吸纳就业3人。专注正规医疗护理服务,主营医院专属陪护与居家上门护理,覆盖术后康复、失能半失能照料、慢病护理、偏瘫照护、生活监护等全场景,与医疗机构深度合作。", "**成长5|南菌优培——食用菌专用生物制剂创新与高原特色野生菌高值化全链开发(项目)**\n- 负责人:王荣飞,2026年毕业于中山大学外科学专业。\n- 简介:三年以上研发基础与产业化探索,核心团队含分析化学博士、农业硕士、医学硕士等。计划2026年7月完成公司注册并启动。聚焦食用菌专用生物制剂(引入海藻糖-6-磷酸、寡糖等生物源成分)与高原特色野生菌(硬皮地星、硬皮马勃)高值化全链开发。", "**成长6|五华区丽裳文化艺术工作室**\n- 负责人:王娟,2024年毕业于昆明幼儿师范高等专科学校幼儿教育专业。\n- 简介:2026年4月成立,专注传统文化、民俗活动、茶文化及国风品牌活动策划执行与文旅研学。依托云南自贸区优势,推动民俗文创、茶产品、干花制品及高原特色产品出海,搭建“账号引流+内容传播+产品出海”运营体系。", "**成长7|云南星瑞航空(项目)**\n- 负责人:尹永浩,2019年毕业于滇西科技师范学院亚洲微电影学院数字媒体技术专业。\n- 简介:2026年4月启动,专注低空智能应用,以大疆行业级无人机为核心装备,业务涵盖无人机测绘、三维建模、数字孪生、应急救援、公共安全巡检、夜间红外搜救及电力、光伏、油气管道、森林防火等作业,并深耕文物数字化保护。具备硬件销售、数据处理、技术支持、人才培训一体化能力。", "**成长8|综合性云南直播私域服务平台**\n- 负责人:金满满,2026年7月毕业于周口师范学院学前教育专业。\n- 简介:综合性云南直播私域服务平台,深耕云南本土特色资源,打造自主IP形象,以短视频创作+全域直播运营为核心,搭建全国性自主私域流量池;同时搭建大学生创业孵化通道,构建人才迭代体系。", "### D. OPC创业空间\n(场地安排=OPC,共6家)", "**D1|中外青少年研学(项目)**(OPC)\n- 负责人:王得荣,2022年武汉大学法学专业毕业,2024—2025年英国剑桥大学短期交流学习。\n- 简介:国际游学与教育交流服务项目,已与美国、英国、澳大利亚、新加坡等多国顶尖教育机构达成长期直营战略合作,为青少年、在职教师及家长提供一站式全球化教育交流解决方案。", "**D2|星杭商务信息咨询昆明有限责任公司**\n- 负责人:杨之华,2024年6月毕业于云南师范大学教育学专业。\n- 简介:主要经营商务服务、信息咨询培训、商务接待及服务保障配套。", "**D3|蓝智科技(项目)**\n- 负责人:徐安厅,2026年毕业于昆明城市学院计算机科学与技术专业。\n- 简介:OPC一人公司,2026年5月启动,从业3人。立足西南蓝莓特色种植产业,专注农业物联网技术研发与智慧基地数字化运营,自研蓝智全系列智慧种植监控系统,涵盖田间环境监测、基质养分检测、水肥灌溉、种植生产数据管理等。", "**D4|云品出滇·纸享万家(项目)**\n- 负责人:王帅,2024年毕业于昆明理工大学电子信息工程专业。\n- 简介:2026年3月成立,以新型悬挂式抽纸为核心切入产品,依托AI工具与电商平台,打造国内电商+跨境电商双增长模式的日用纸品品牌。当前已在拼多多平台实现真实销售验证,未来向东南亚跨境电商扩展。", "**D5|云迹轻旅——昆明本土一站式轻量化文旅服务工作室(项目)**\n- 负责人:王海璇,云南大学工商管理(MBA)毕业,10年云南文旅从业经验。\n- 简介:单人OPC轻资产运营,构建“内容+服务+技术”三位一体业务矩阵:文旅自媒体IP孵化、私人小众旅行定制、非遗自然研学小团、文旅商业咨询策划、AI数字文旅技术服务五大板块。面向C端游客与B端本地文旅商家提供服务。", "**D6|启元人工智能科技(项目)**\n- 负责人:李瑞康,2017年毕业于吉林动画学院广播电视编导专业。\n- 简介:OPC一人公司,专注8-12岁儿童人工智能工具的技术推广与科普体验服务,自主研发适配低龄儿童的AI实操工具包,依托共享空间开展少儿AI创意科技体验沙龙(AI绘画、AI音乐创作、AI视频制作)。", "> 其他在简介中表述为“OPC一人公司”但按场地安排归入其他分区的项目:云南派音人工智能科技(加速1)、昆明舒护安养老服务(成长4)、云南大学AI+联合创业服务平台(加速10)等。\n> 另:云南大学AI+联合创业服务平台完整介绍见【孵化加速区-加速10】。", "### E. 一楼直播间\n(共1家)", "**一楼直播间|昆明屿澈电子商务有限公司**\n- 负责人:杨君武,2024年6月毕业于云南交通职业技术学院建设工程管理专业。\n- 简介:高校毕业生发起的东南亚跨境电商AI智能直播+短视频出海项目,依托TikTok Shop正规跨境渠道,面向马来西亚、泰国、越南、菲律宾四国,主营日用百货、家居用品、电子配件、文创收纳、便携小家电等。采用AI数字人7×24小时无人直播、AI短视频批量引流、1688一件代发轻资产模式。", "### F. 信息表中“场地安排”列为空的其他项目\n《入驻企业信息表》中以下项目(共8家)的“场地安排”列未标注分区,照原文列出供全量归档:\n**云南上古绝学文化发展有限公司(简称:上古书院)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:赵泳淇,2024年毕业于贵州大学教育技术学专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:专注上古家学文化传承与国学赋能的文化机构,以“修身齐家”为核心理念,业务涵盖家学文化课程体系研发、国学经典文化交流、家训家规个性化定制及文化研学,走“从文化体验到家族传承”路径。", "**中越生物医疗(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:武妙英(VŨ DIỆU ANH),越南籍留学生,云南大学工商管理专业在读,精通越语、法语、中文、英语。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:聚焦越南标准化生物蛋白生产联合实验室建设,计划覆盖区域80%以上缺口市场,依托中越技术协同与资源互补开展高纯度生物活性蛋白合规化跨境生产与出口,满足疫苗、诊断试剂及医疗器械领域需求。已获越南五部门联合认证。", "**中越文旅交流(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:阮胡竹珠,云南民族大学广播电视专业在读(2027年毕业)。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:搭建中越文化旅游交流平台,提供旅游咨询、行程定制、文化体验、语言服务及跨境旅游推广,依托广播电视专业背景与中越双语优势开展中越旅游宣传内容制作、新媒体运营、跨境文化交流活动策划。", "**“绿态”环保科技项目**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:陈国旗,云南民族大学在读留学生,负责海外工厂对接、中越跨境供应链统筹及中国全渠道分销网络核心运营。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:紧抓“禁塑令”及绿色环保政策导向,专注天然槟榔叶(mo cau)环保餐具研发、跨境贸易与新媒体全渠道销售。在越南本土自有实体生产工厂,100%越南本土完成从源头集采到杀菌包装,成品经正规清关出口至中国,在昆明园区设“中国市场总分销库与营销/直播工作室”,构建“抖音直播+货架电商双驱闭环”。", "**昆明滇油石化有限责任公司**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:李政德,预计2027年毕业于云南国土资源职业学院地球物理勘探专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:专注生物基纳米驱油剂的研发、生产与销售,为国内低渗透油藏及老油田二次/三次采油提供绿色高效增产解决方案。目标用户:中石油(华北、长庆、新疆、大庆等)、中石化(胜利、中原等)、中海油(渤海等)下属老油田;延伸至一带一路沿线国家老油田(中亚、中东、非洲)及民营油服企业(如延长石油)。", "**Facebook越南市场普洱茶与建水紫陶壶跨境电商(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:罗有艳,2020年毕业于云南农业大学越南语专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:依托Facebook全域流量生态(主页、直播、短视频、广告),聚焦越南垂直市场,主营普洱茶、手工茶具(云南建水紫陶小品壶及公道杯组合)、中式茶礼套装跨境零售。借助“内容种草+直播转化+9610跨境报关+越南专线直邮”闭环模式。", "**鬼才明AI创意工作室(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:赵广明,2022年毕业于重庆大学计算机专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:2026年4月启动,聚焦产教融合、创新创业赛事运营与数智化品牌赋能,围绕AI技术商业化落地、新商科专业规划、全品类活动策展及职业技能人才培育开展服务。搭建AI智能交互体系与AI数字人内容输出矩阵,为政校企提供AI场景应用、全域获客、职业技能培训、赛事集训、资格认证、品牌全案、项目申报等全链路解决方案。", "**昆明云韵体育有限责任公司 / 男子自行车洲际队(项目)**", "4. 入驻企业完整名录(39家/信息表)\n- 负责人:舒江贵,2020年7月毕业于麒麟区职业学校运动训练专业。", "4. 入驻企业完整名录(39家/信息表)\n- 简介:2026年4月发起,拟在云南注册成立UCI男子自行车洲际队,打造立足云南、辐射全国、征战亚洲赛场的职业自行车竞技项目,依托云南高原训练优势,构建“职业赛事+商业赞助+高原训练+体育文旅+青苗人才培养”一体化运营模式。", "> 注:申报名单(第5节)将该“男子自行车洲际队项目”归类为 OPC。\n---", "> 出处:附件1.3 创业孵化载体入驻实体名单(省级申报口径,31家)\n| 序号 | 入驻经营实体或项目团队名单 |\n|---|---|\n| 1 | 昆明舒护安养老服务有限责任公司 |\n| 2 | 星杭商务信息咨询(昆明)有限责任公司 |\n| 3 | 达岸教育管理(云南)有限公司 |\n| 4 | 五华区丽裳文化艺术工作室 |\n| 5 | 米勒克尔蓝宝石(项目) |\n| 6 | 中越生物合作——高纯度鲎血蛋白提取的跨境技术创新和商业推广(项目) |\n| 7 | 云南宸中低空经济有限公司 |\n| 8 | 中泰教育合作(项目) |\n| 9 | 人工智能机器人训练(项目) |\n| 10 | 欧森纳东盟香港留学(项目) |\n| 11 | 仰光客厅中缅文化体验营地(项目) |\n| 12 | PineSound派音智能——下一代音效与配乐基座(项目) |\n| 13 | 千楣之路——缅甸“千年之木”Thanaka非遗文化传播与跨境贸易(项目) |\n| 14 | 昆明朵哈食品有限公司 |\n| 15 | 滇缅一带一路国际化建设设计(项目) |\n| 16 | 云南定影文化传播有限公司 |\n| 17 | 酷享野农AI农业 |\n| 18 | 昆明智海银高文化科技有限公司 |\n| 19 | 鬼才明AI创意策划工作室(项目) |\n| 20 | 昆明就惯你生物科技(项目) |\n| 21 | 联翼航空技术低空经济应用国际化拓展(项目) |\n| 22 | 中外青少年研学交流(项目) |\n| 23 | 昆明瀚颖教育信息咨询有限公司 |\n| 24 | 花仙子园艺肥料(云南)有限公司 |\n| 25 | 云南星瑞航空科技(项目) |\n| 26 | 男子自行车洲际队项目(OPC) |\n| 27 | 云南廷秀文旅康养服务项目(OPC) |\n| 28 | 中通乐创(项目) |\n| 29 | 跨境电商出口运营项目 |\n| 30 | 华寮国际教育交流(云南)有限责任公司 |\n| 31 | 综合性云南直播私域服务平台 |\n> 说明:本名单项目名称部分与《入驻企业信息表》略有出入(如“米勒克尔蓝宝石”、", "“欧森纳东盟香港留学”、中越生物改名等),系两表不同填报口径/时点所致,均照原文保留。\n---", "> 出处:昆明市大学生创业园创业企业(项目)入驻管理规程.wps(昆明市人力资源和社会保障局,2026年3月13日)", "### 一、实施主体", "6. 企业(项目)入驻管理规程要点\n- 运营主管:昆明市人力资源和社会保障局(主管部门)", "6. 企业(项目)入驻管理规程要点\n- 执行:通过政府购买服务委托第三方服务机构(服务机构),负责入驻全流程工作;主管部门行使监督、考核、验收职权。", "### 二、入驻条件", "6. 企业(项目)入驻管理规程要点\n- **对象范围**:在校大学生及毕业8年内高校毕业生、归国留学生、外籍留学生等青年创业企业(项目),申请人须担任法定代表人/项目负责人。符合昆明市产业发展的优质企业(项目)或OPC企业可适当放宽。", "6. 企业(项目)入驻管理规程要点\n- **产业方向**:聚焦昆明市“10+N”产业链——数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等,具备创新性、可操作性、市场前景良好。", "6. 企业(项目)入驻管理规程要点\n- **其他要求**:明确可行的经营计划、启动资金和风险承担能力,规范的企业管理制度;无知识产权纠纷、违法犯罪记录、经营异常记录;守法经营、诚实守信,遵守园区各项规定。", "6. 企业(项目)入驻管理规程要点\n- **优先入驻**:获得市级及以上创业大赛奖项、突出学术成就、重大发明创造及高新专利技术的企业(项目)。", "### 三、入驻流程", "6. 企业(项目)入驻管理规程要点\n1. **提出申请**:提交《入驻申请表》《创业计划书》、身份证(外籍提供护照/签证)、学历证明、营业执照(已注册)、资质专利证书等。", "6. 企业(项目)入驻管理规程要点\n2. **项目初审**:服务机构初审,材料不齐一次性告知补全。", "6. 企业(项目)入驻管理规程要点\n3. **入驻评审**:服务机构从《昆明市就业创业导师库》抽取3名评审专家,路演答辩评价,量化打分制(100分),80分及以上通过。", "6. 企业(项目)入驻管理规程要点\n- 团队资质与能力 **20分**(学历背景、专业匹配度、创业经验、组织管理、人员稳定)", "6. 企业(项目)入驻管理规程要点\n- 项目创新性与技术壁垒 **25分**(核心技术/商业模式创新点、自主知识产权、技术领先性、行业壁垒)", "6. 企业(项目)入驻管理规程要点\n- 产业适配与市场前景 **25分**(与重点产业链匹配度、市场需求、竞品、市场拓展、盈利模式)", "6. 企业(项目)入驻管理规程要点\n- 运营规划与落地能力 **15分**(商业计划完整性、启动资金、经营管理制度、阶段目标、落地方案)", "6. 企业(项目)入驻管理规程要点\n- 社会价值与发展潜力 **15分**(带动就业能力、税收贡献预期、行业潜力、可持续发展)", "6. 企业(项目)入驻管理规程要点\n4. **评审公示**:报主管部门备案后,在市人社局官网公示,无异议准予入驻。", "6. 企业(项目)入驻管理规程要点\n5. **签署协议**:签订《入驻孵化协议》,分配办公场地/工位,正式入驻。", "### 四、入驻周期(梯度孵化四大区域)\n| 孵化区域 | 面向对象 | 配套 | 孵化周期 |\n|---|---|---|---|\n| OPC创业空间 | 以AI为支撑、聚焦数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域 | 拎包入驻的独立办公位或共享工位 | 最长2年 |\n| 创业苗圃区 | 创意阶段、未注册企业的大学生创业团队 | 开放式共享工位 | 最长2年(落地成长好可推荐转加速区1年) |\n| 孵化加速区 | 已完成工商注册、", "处于初创期的企业(项目) | 独立办公空间 | 基础2年,评估合格可延1年,累计≤3年 |\n| 国际创客区 | 归国留学生、外籍留学生创业企业(项目) | 个性化办公空间+国际化配套服务 | 参照孵化加速区 |", "### 五、项目年度考核", "6. 企业(项目)入驻管理规程要点\n- **考核维度**(量化打分制,100分):经营发展能力(20):营收/利润/纳税、经营计划完成度、管理制度完善度;技术创新能力(25):知识产权新增、研发投入、产品/服务迭代、产学研合作;合规与履约情况(20):遵守管理制度、协议履约、场地规范、材料报送、守法经营;创业带动成效(20):新增就业(高校毕业生占比)、实习实训岗位;发展潜力与孵化成效(15):融资进展、资质认证、市场拓展、可持续规划、出孵成熟度。", "6. 企业(项目)入驻管理规程要点\n- **考核等级**:优秀(80分及以上)、合格(60–79分)、不合格(60分以下)。", "6. 企业(项目)入驻管理规程要点\n- **结果处理**:合格准予继续孵化(期满可申请延期);不合格下达《整改通知书》,整改期满仍不合格的可解除孵化协议、勒令限期退出。", "### 六、退出管理(四种类型)", "6. 企业(项目)入驻管理规程要点\n1. **成功孵化出园**:满足任一成熟出孵标准——①产权明晰,有自主核心产品并规模化;②经营良好,产品有稳定市占率与盈利;③制度健全、组织完备、团队稳定;④已获行业经营资质、质量稳定;⑤有持续研发能力与规划。", "6. 企业(项目)入驻管理规程要点\n2. **孵化期满退出**:周期届满未申请延期或延期未通过、已延期到期。", "6. 企业(项目)入驻管理规程要点\n3. **主动申请退出**:因经营问题、团队变动等申请退出。", "6. 企业(项目)入驻管理规程要点\n4. **强制勒令退出**:①协议签订后累计30个工作日无人到现场办公,或连续7个工作日空置且未报备累计超3次;②违反规定造成重大经济损失/名誉损失;③不服从管理情节严重;④被停业、吊销执照、破产清算;⑤违法经营;⑥私自转租、变更经营项目;⑦虚报数据、伪造材料、拒不配合。", "> 出处:空间布局.png(概念布局)与 详细的布局.png(分区布置图)\n**概念布局(空间布局.png)**:顶部左侧为“玻璃顶”(开敞中庭/天光区);上部右侧以“过道”纵贯,两侧各若干“房间”;中上部为“大厅”;中部横向依次为若干“房间”、橙色的“共享工位区”、绿色的“休息厅”;底部由完整“过道”贯通,下接一排9间“房间”。", "**分区布置(详细的布局.png)**:\n| 区域类别 | 编号/名称 | 数量 |\n|---|---|---|\n| 成长区 | 成长区01–08 | 8 |\n| 国际区 | 国际区01–07 | 7 |\n| 加速区 | 加速区01–13 | 13 |\n| 共享办公 | 共享工位区 | 1 |\n| 会议空间 | 小会议室 / 大会议室 | 2 |\n| 功能空间 | 玻璃顶(中庭)、休息厅、过道、", "楼道 | 若干 |\n> 布局总览:上部左侧为“玻璃顶”开敞区,右上依次为成长区与国际化两纵列;中部靠上为共享工位区与休息厅;下方以“过道”为轴,分布加速区、大小会议室及楼道。分区命名与《入驻企业信息表》中的“加速/国际/成长/OPC”场地安排完全对应。\n---", "> 出处:昆明市创业园宣传册.pdf(16页彩页;以下为正文要点,图片部分略)", "### 8.1 园区定位与理念", "8. 宣传册核心内容(园区简介·服务·政策)\n- 云南省首家由政府部门主办、为创业青年大学生提供**免费创业孵化服务**的**开放式园区**。", "8. 宣传册核心内容(园区简介·服务·政策)\n- 2009年正式成立,昆明市人力资源和社会保障局主办运营,2012年获评省级创业园;位于民航路229号昆明市人力资源中心1–2楼,总面积约3000平方米。", "8. 宣传册核心内容(园区简介·服务·政策)\n- 以助力青年大学生创业梦想为核心,构建 **“空间+孵化+融资+政策+资源+AI赋能+综合服务”七位一体**全链条创业扶持体系,打造“一站式”创业孵化服务平台。", "8. 宣传册核心内容(园区简介·服务·政策)\n- 紧扣面向南亚东南亚的国际定位,融合AI前沿技术,打造现代化创业/创新服务平台,成为青年人才来昆留昆创业创新的“首站”与“家园”。", "### 8.2 政策服务(七大板块)", "8. 宣传册核心内容(园区简介·服务·政策)\n1. **空间保障**:免费办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等基础办公设施。", "8. 宣传册核心内容(园区简介·服务·政策)\n2. **孵化服务**:专业创业指导专家团队提供创业规划、技术指导、商业模式梳理、市场对接等“一对一”服务;定期举办创业沙龙、创业讲堂、项目路演。", "8. 宣传册核心内容(园区简介·服务·政策)\n3. **融资支持**:联动省内外创投机构,设立创业种子资金/合作投资基金,提供政策性贷款、商业银行贷款、股权融资等推荐指导。", "8. 宣传册核心内容(园区简介·服务·政策)\n4. **政策对接**:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策。", "8. 宣传册核心内容(园区简介·服务·政策)\n5. **资源整合**:对接驻昆高校、科研院所、龙头企业,提供技术合作、成果转化、人才招聘;纳入昆明创业创新服务体系,优先参与“春城创业荟”等品牌活动。", "8. 宣传册核心内容(园区简介·服务·政策)\n6. **专项赋能培训**:定期开展AI公益性培训,提供AI应用、数字内容创作、一人公司创业落地、企业AI降本增效等公益培训与实战辅导。", "8. 宣传册核心内容(园区简介·服务·政策)\n7. **其他服务**:提供工商注册、税务代办、知识产权咨询、法律援助等专业化服务。", "### 入驻条件(宣传册口径,与管理规程一致)\n(一)**对象范围**:在校大学生及毕业8年内高校毕业生、归国留学生、外籍留学生等青年创业企业(项目),申请人须担任法定代表人/项目负责人;优质项目或OPC企业可放宽。\n(二)**产业方向**:聚焦昆明市“10+N”产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等),具备创新性、可操作性、市场前景良好。\n(三)**其他要求**:明确可行的经营计划、", "启动资金和风险承担能力,规范管理制度;无知识产权纠纷、违法犯罪及经营异常记录。\n(四)**优先入驻**:获得市级及以上创业大赛奖项、突出学术成就、重大发明创造及高新专利技术的项目。", "### 入驻流程(宣传册六步)\n咨询报名 → 提出申请 → 项目初审 → 入驻评审(路演答辩,量化打分≥80分通过)→ 评审公示(市人社局官微公示)→ 签署协议、分配场地工位、正式入驻。\n---", "| 区域 | 对象 | 配套 | 周期 |\n|---|---|---|---|\n| **OPC创业空间** | AI为支撑,聚焦数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域 | 拎包入驻独立办公位或共享工位 | 最长2年 |\n| **创业苗圃区** | 创意阶段、未注册企业的大学生创业团队 | 开放式共享工位 | 最长2年(成长好可转加速区1年) |\n| **孵化加速区** | 已工商注册、", "初创期企业 | 独立办公空间 | 基础2年,评估合格延1年,累计≤3年 |\n| **国际创客区** | 归国留学生、外籍留学生 | 个性化办公空间+国际化配套 | 参照孵化加速区 |\n---", "11. 报名方式\n- 咨询电话:18087174891、18214573650(宣传册);18087174891(统计表填报人);云南省就业局/园区申报口径见第3节填报信息", "11. 报名方式\n- 咨询地址:昆明市民航路229号(昆明市人力资源中心二楼 昆明市大学生创业园)", "11. 报名方式\n- 接收邮箱:714066050@qq.com", "**媒体报道矩阵**:整合电视、广播、报纸、新媒体等资源,通过电视专栏、广播专题、报纸专版、网络直播、地铁专列、新媒体推广等方式,全方位宣传报道园区创业创新典型事迹。\n**合作媒体机构**(宣传册列示):", "12. 媒体报道与合作媒体\n- 全国主流:央视网(CCTV.com)、中国网(China.org.cn)、人民网(people.cn)、今日头条、网易、搜狐、抖音", "12. 媒体报道与合作媒体\n- 省级媒体:云南日报、云南广播电视台(Yunnan Media Group)、云南网、春城晚报", "12. 媒体报道与合作媒体\n- 市级媒体:昆明广播电视台、昆明日报、掌上春城(KMCC)、无线昆明", "12. 媒体报道与合作媒体\n- 其他:8099999 新闻热线等", "**重要外宣数据口径(宣传册/新闻引用,酌情采用)**:", "12. 媒体报道与合作媒体\n- 昆明市政府网站:园区运营超过10年,入驻企业超1000家、带动创业者超1万人次(2024-04报道口径)。", "12. 媒体报道与合作媒体\n- 春城晚报:近3年累计孵化企业超100家,培育国家级高新技术企业5家、省级科技型中小企业12家。", "12. 媒体报道与合作媒体\n- 昆明日报:2024年以来新增入驻企业32家、其中高新技术企业15家;举办各类活动126场,覆盖青年创业者超2万人次。", "12. 媒体报道与合作媒体\n- **提示**:以上为媒体报道口径,与省级申报/月报表中的官方累计口径(累计孵化181家、出园149家、带动就业1529人等)存在差异,对外引用时请注明来源。", "| 时间 | 事件 |\n|---|---|\n| 2009年9月 | 昆明市大学生创业园成立 |\n| 2012年 | 获评省级创业园 |\n| 2024-04-15 | 昆明市大学生创业园2026年第二期入驻招募公告发布(同时发布第一期评审结果公示) |\n| 2024-04-27前后 | 园区经全周期优化后“焕新亮相”获多家媒体报道(央视网5G、中国网、昆明市政府网、昆明日报、", "8099999等),报道时间多为2024-04-28 |\n| 2024-05-08 | “聚力赋能·共创未来”高校创新创业交流暨项目路演活动(主办:园区;协办:滇池学院、昆明城市学院、五华区大学生创业联盟) |\n| 2024-05-10 | 昆明城市学院师生赴园区“取经”参观交流 |\n| 2024-05-12 | 春城晚报、", "云南日报整版深度报道《创业有园 未来可期》 |\n| 2024-05-13 | “昆明市青年企业家协会走进园区暨创新创业基地揭牌仪式” |\n| 2024-05-14 | “云南高校·昆明市大学生创业园”就业创业交流活动暨创新创业基地揭牌仪式(云南经济管理学院、昆明技师学院80余人参加) |\n| 2026-03-13 | 《入驻管理规程》印发(昆明市人力资源和社会保障局) |\n| 2026-03-20 | 2026年第一期入驻申请受理截止:收到申请38个,25个通过初审、", "13个提交复审 |\n| 2026-03-24 | 第一期集中评审(30个路演参评,评审走“资格复核—现场路演—专家提问—综合评议”,量化打分) |\n| 2026-03-25 | 《工作简讯〔2026〕第1期》:第一期最终确定23个优质企业(项目)正式入驻 |\n| 2026-04-15 | 发布2026年第二期入驻招募公告 |\n| 2026-04-24 | “创赢未来”2026创业大赛昆明选拔赛暨马兰花创业培训讲师大赛落幕(主办:昆明市人社局;", "承办:昆明市公共就业和人才服务中心;4月20日起4天路演,13个项目晋级省级大赛) |\n| 2026-05-06 | 《工作简讯〔2026〕第2期》:此梳理第一期23家入驻企业全覆盖实地摸排,2家长期停滞企业督促整改 |\n| 2026-05-20 | “滇水逐梦·雨林创航”2026昆明·西双版纳创业路演PK赛(西双版纳州公共就业和人才服务中心、滇池学院、园区联合主办;园区企业鬼才明AI活动策划工作室、", "云南定影文化传播有限公司协办) |\n| 2026-06-03 | 云南旅游职业学院赴园区开展就业创业交流活动 |\n| 近期(2026) | 云南师范大学与园区联合举办就业创业交流活动暨就业创业实践基地揭牌仪式(开启“校地产教融合、跨境双创人才共育”,缅甸、越南、印尼、老挝等国留学生参加) |\n---", "> 出处:昆明市大学生创业园 定稿(.ai 源文件 + 制版PNG/JPG)", "14. 品牌VI设计规范\n- **Logo**:橙、蓝双色组合标识——橙色“K”与深蓝“M”交织成圆环形/动感造型,英文全称“KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK”,主推版式居中,品牌色橙蓝相衬。", "14. 品牌VI设计规范\n- **品牌主色(设计稿拾色器取值)**:", "| 色彩 | Hex | R | G | B | 说明 |\n|---|---|---|---|---|---|\n| 主色·橙 | `#FC8318` | 252 | 131 | 24 | 活力橙,主标识/强调 |\n| 辅色·深蓝 | `#022B97` | 2 | 43 | 151 | 深邃蓝,主标识/正文 |", "14. 品牌VI设计规范\n- **配套文件**:`昆明市大学生创业园 定稿.ai`(矢量源文件)、`_画板 1.png/.jpg`(主画板成品)、`_画板 1 副本.png`(色彩规范参考)。", "### 附注:数据口径说明", "14. 品牌VI设计规范\n1. **累计数与当年数**:第2节绩效、第3节月报表中均区分“认定以来累计 / 本年度 / 当前实有”。本月报表中累计出园149户与省级申报累计出园150户相差1户,为两表填报时点不同所致,均照原文保留。", "14. 品牌VI设计规范\n2. **入驻规模**:信息表(39家)面向内部全量项目;省级申报名单(31家)为认定口径,项目名称个别有别。", "14. 品牌VI设计规范\n3. **宣传册数据**(媒体报道/外宣口径)与官方申报累计口径存在差异,引用时务必注明出处(见第12节提示)。", "14. 品牌VI设计规范\n4. 本文件所列数值均直接源自原始文件,未作推算加工。", "---\n*文档生成日期:2026年 · 依据 `.xlsx / .xls / .wps / .pdf / .png` 全量原始资料整理*"], "questions": [["这个知识库里的政策和数据是最新版的吗?", "补贴金额和申报条件以哪里为准?", "你们说的截至2026年8月是什么意思?", "如果官方文件更新了,这里的内容还准不准?", "这个知识库主要是用来干啥的?"], ["一个人能开公司吗?", "什么叫一人公司?OPC是啥意思?", "一人有限责任公司和一人股份有限公司有啥区别?", "只有一个股东的公司合法吗?", "OPC是不是就是只有一个老板的公司?"], ["一人公司是啥时候开始在中国合法的?", "2005年《公司法》修订跟一人公司有啥关系?", "以前为啥不允许一个人开公司?", "一人有限责任公司最早是哪年写进法律的?", "中国是从什么时候起承认一人公司的?"], ["AI时代为啥适合搞一人公司?", "一个人用AI能干成哪些事?", "现在一个人创业靠AI真能搞定研发、营销、客服和财务吗?", "为啥说一人公司是低成本高效率的创业方式?", "AI时代的一人公司跟传统小作坊有啥不一样?"], ["‘你负责商业判断,AI负责高效执行’这句话啥意思?", "一人公司怎么从卖时间变成经营系统?", "怎么理解‘经营自动化系统’这个说法?", "一人公司里人和AI具体怎么分工?", "这句话说的‘跨越’到底指什么变化?"], ["一人公司和自由职业者有啥区别?", "OPC算不算正规公司?跟个体户、 freelancer比有啥优势?", "一人公司是不是有限责任?架构是不是特别简单?", "为啥说一人公司税负更可控?它和普通公司比灵活在哪?"], ["现在全国有多少家一人公司?", "截至2025年6月,一人有限责任公司总数大概多少?", "一人公司数量破千万了吗?最新统计数据是多少?", "全国一人公司注册量现在到什么规模了?"], ["新《公司法》对一人公司是放宽还是收紧了?", "2024年7月1日实施的新公司法,对一人公司主要有哪些变化?", "新公司法下一人公司是更容易注册了还是责任更大了?", "新《公司法》对一人公司的总原则是什么?"], ["一个人能开股份有限公司吗?", "新公司法允许一个自然人设立股份有限公司吗?", "一人股份公司合法吗?需要几个股东才能成立股份公司?", "以前说股份公司至少两人,现在改了吗?"], ["一个人还能注册多家一人公司吗?", "新公司法取消了‘一个自然人只能开一家一人公司’的规定吗?", "我之前注册了一人公司,还能再注册第二家吗?", "一人公司之间还能互相投资成立新公司吗?"], ["新公司法实施后,个体工商户能自己开一人公司吗?", "合伙企业可以当一人公司的股东吗?", "个人独资企业现在能注册成一人公司的老板不?", "2024年7月1号起,除了自然人和法人,还有哪些主体能当一人公司股东?", "新《公司法》是不是把一人公司股东范围扩大到个体户、合伙企业这些了?"], ["新公司法还分‘一人有限责任公司’这种专门类型吗?", "现在注册只有一个股东的公司,营业执照上还写‘一人有限公司’吗?", "新法里是不是取消了‘一人股份有限公司’的说法?", "只有一个股东的公司,现在法律上怎么称呼?还叫‘一人公司’吗?", "新《公司法》是不是把一人公司直接当成普通公司来管,不再单列一节了?"], ["新公司法下,一人公司每年还要强制做审计吗?", "只有一个股东的公司,2024年7月后还必须请会计师事务所审账吗?", "现在一人公司还能自己编财务报告,不用找人审计不?", "新《公司法》取消了对一人公司的年度审计硬性要求,是真的吗?", "一人公司今年开始是不是不用每年都搞财务审计了?"], ["新公司法实施后,营业执照上还会标‘自然人独资’或‘法人独资’吗?", "注册一人公司时,登记信息里还要特别注明是独资性质吗?", "现在办营业执照,系统还让选‘自然人独资’这个选项吗?", "一人公司的工商登记和执照上,是不是不再体现‘独资’字样了?", "新法取消了独资标注要求,那别人怎么知道这家公司是一个人开的?"], ["新《公司法》对一人公司最大的责任变化是什么?", "一人公司股东现在要承担什么更强的责任?", "听说新公司法加强了一人公司的股东责任,具体指哪块?", "为什么说‘责任强化’是重点注意事项?会带来什么实际影响?", "一人公司老板在新法下要特别注意哪些法律责任风险?"], ["新公司法实施后,一人公司的股东如果没法证明公司财产和自己个人财产分开,是不是要对公司债务一起负责?", "2024年7月1日新公司法生效后,一人公司股东举证责任倒置具体指什么?", "一人有限公司的老板要是说不清公司钱和自己钱的区别,会不会被要求连带还债?", "新《公司法》第23条第三款关于一人公司股东连带责任的规定,到底怎么理解?", "一人公司股东怎么避免因财产不独立而承担连带责任?新法里举证责任倒置是啥意思?"], ["法院一般怎么判断一人公司存在财产混同?账户之间转钱多算不算?", "公司账上和个人银行卡老有资金往来,会被认定为财产混同吗?", "一人公司股东和公司之间经常互相转账,是不是很容易被认定为财产混同?", "司法实践中,认定一人公司财产混同最主要看什么?是不是就看银行流水?", "公司跟股东之间频繁打款,是不是财产混同最直接的证据?"], ["用我自己的微信/支付宝订阅ChatGPT Plus,算不算公司财产不独立?", "AI时代下,用个人账号买大模型服务、存训练数据,会不会让一人公司被认定为没独立财产?", "我把公司用的AI算力、生成内容都存在个人云盘里,新公司法下有风险吗?", "以个人名义开通的云端GPU服务,给公司项目用,会被认为财产混同吗?", "在个人账号里管理公司AI资产(比如API密钥、训练数据、生成结果),新法实施后会否导致股东担责?"], ["一人公司没有股东会,利润怎么分?必须写成书面文件吗?", "新公司法要求一人公司股东做决定必须签字留底,哪些事必须书面决定?", "一人公司修改章程或分利润,不开会的话,光口头说行不行?", "股东自己决定分红或补亏,是不是得签个字或者盖个章,然后放在公司备查?", "新《公司法》规定一人公司重大事项须书面决定,具体包括哪些?怎么操作才合规?"], ["一人公司要怎么管账才算符合新公司法要求?", "新公司法强调财务独立,一人公司是不是必须单独建账、单独报税?", "公司账和老板私人账混着记,新法下有什么风险?", "一人公司如何做到账目清晰、独立核算?有没有实操建议?", "建立健全财务管理制度对一人公司来说,具体要落实到哪些动作上?"], ["新公司法实施后,一人公司怎么证明财产独立?需要做哪些审计?", "2024年7月1日新公司法生效后,一人有限公司必须每年做审计报告吗?", "一人公司股东和公司之间资金往来,要怎么审计才能避免连带责任?", "新《公司法》要求一人公司提供连续完整的会计年度审计报告,具体指什么?", "除了年度审计,一人公司还能用什么审计报告来证明财产独立?"], ["新公司法说的‘数据与数字资产隔离’是什么意思?跟以前的财务隔离有啥不同?", "一人公司现在不仅要管好钱,还要管好数字资产?具体要怎么做?", "新《公司法》对一人公司的合规要求是不是从钱扩展到数据了?", "什么叫‘数字资产隔离’?一人公司怎么确保客户数据、AI模型这些不和股东个人混同?", "新公司法提到的数据隔离,是指公司微信账号、域名、数据库这些也要和股东分开吗?"], ["AI时代创业还用等学完所有技能再开始吗?", "听说现在创业讲究‘先完成再完美’,是不是真的不用准备充分就能启动?", "AI把创业门槛降低了,那新手到底该什么时候开始做第一个项目?", "‘先完成,再完美’在OPC创业里具体怎么操作?举个例子?", "不会编程、不会设计,能直接用AI开公司吗?需要先学多久?"], ["最小成本验证模式具体怎么操作?比如第一步该做什么?", "用AI做决策+外包干活,这种最小验证方式适合哪些类型的创业项目?", "没多少启动资金,怎么用最小成本验证我的商业想法靠不靠谱?", "OPC创业中说的‘最小成本验证’,和MVP(最小可行产品)是一回事吗?", "AI思考+外包人力,这个组合在验证阶段容易踩哪些坑?"], ["OPC创业说的‘四力’是哪四力?分别指什么?", "产品能力、流量能力、交付能力、变现能力,这四个能力怎么判断自己有没有?", "创业初期资源有限,应该优先提升四力中的哪一力?", "为什么OPC特别强调这四个能力?少了其中一力会有什么风险?", "怎么评估自己的流量能力和变现能力是否匹配?有没有简单自测方法?"], ["盈利三把钥匙具体指哪三个?", "商业模式里说的‘盈利三把钥匙’是什么意思?", "怎么理解用AI降本增效、用信任创造溢价、用平台借势腾飞这三句话?", "OPC提到的盈利三把钥匙,有没有更接地气的解释?", "盈利三把钥匙和普通创业方法有啥不一样?"], ["OPC推荐的主流商业模式有哪些?", "做个人创业,有哪些靠谱的变现方式?", "知识付费、内容变现、服务外包这些模式具体都包括啥?", "SaaS工具、付费社群、混合模式分别适合什么人做?", "有没有把六大商业模式讲清楚的例子或对比?"], ["OPC创业落地的完整步骤是怎样的?", "从零开始做个人创业,该按什么顺序一步步来?", "怎么用最低成本验证一个想法靠不靠谱?", "什么是MVP测试?私域池和自动化引流矩阵怎么搭?", "怎么设计能抗风险的收入结构?标准化交付系统怎么做?"], ["电商创业案例里那个河南创业者是怎么做到月入几万的?", "卧床70天还能做跨境电商?他具体用了哪些AI工具?", "用扣子搭工作流生成经营日报,这个怎么操作?", "12家网店为啥最后只留4家?优化逻辑是啥?", "每天只干2-3小时,单店月净利2.5万以上,普通人能复制吗?"], ["盈利三把钥匙和六大商业模式之间有啥关系?", "创业步骤里提到的‘高溢价产品矩阵’怎么跟‘用信任创造溢价’挂钩?", "AI降本增效在河南电商案例里是怎么体现的?", "自动化引流矩阵和内容变现、社群运营这些模式怎么配合?", "整个OPC创业思路里,最核心、最先该动手的是哪一步?"], ["OPC创业做小红书内容转化服务,具体怎么帮企业提升转化?2025年无锡创业者转型‘企业陪跑’,定价和交易机制这两块到底该怎么解决?", "无锡那边有创业者在2025年搞OPC小红书陪跑服务,听说卡在定价和交付标准化上——这俩难题实际中怎么破?", "OPC模式里说‘个人即产品’、前期没标准交付物,那小红书内容转化服务怎么跟客户谈价格、签合同?", "做企业小红书陪跑的OPC项目,除了内容制作,还包含哪些服务?定价逻辑和交易流程一般怎么设计?", "听说OPC创业在小红书转化这块,最大痛点是定价难+没标准化交付——有没有落地的解决方案或参考案例?"], ["那个全AI员工的公司是怎么运作的?用什么开源AI代理框架搭的?卖教程、抽佣、定制代理这三块业务具体怎么赚钱?", "零人类公司几周赚近20万美元,每月成本才1500美元?它靠哪些AI工具实现全自动运营?教程和技能市场怎么搭建的?", "用开源AI代理做的‘全AI员工公司’,真实案例里AI到底负责哪些环节?企业定制代理和抽佣平台怎么落地?", "不雇人、纯AI跑的公司,收入来源是不是就教程销售、AI技能市场佣金、企业代理定制这三项?每块大概占比多少?", "全AI运营公司月成本只要1500美元,它用的是LangChain还是AutoGen这类框架?技术栈和商业模型能拆解下吗?"], ["35岁大厂管理者裸辞环游世界后做一人公司,具体靠哪些AI相关业务年入近百万?都干了啥角色?", "裸辞后靠AI搞一人公司,同时当大模型工具开发者、企业顾问和自媒体博主——这三重身份怎么协同变现?", "大厂裸辞转AI一人公司,年入百万的真实路径是啥?开发工具、接咨询、做自媒体,哪块收入占比最高?", "35岁裸辞环游多国还能靠AI年入百万,他做的大模型工具有哪些?企业咨询主要解决什么问题?", "一人公司模式下,怎么平衡大模型开发、企业顾问和自媒体三件事?时间分配和收入结构大概是怎样的?"], ["OPC创业里提到的‘清醒认知与挑战’具体指哪些方面?新手最容易踩哪些坑?", "关于OPC创业的清醒认知部分,有没有总结过核心挑战点?比如心态、资源、能力短板这些?", "‘清醒认知与挑战’这块没展开说,实际做OPC时最常被忽略的认知盲区是什么?", "OPC创业者需要具备哪些关键清醒认知?比如对自身能力、市场周期、失败概率的预判?", "除了赚钱门槛没消失,OPC路上还有哪些隐性挑战?比如客户信任建立、长期可持续性、精力管理?"], ["为什么说AI没消灭赚钱门槛?光有工具但没商业逻辑,到底会出什么问题?能举个实际例子吗?", "AI再强也得靠认知和决策能力——这话怎么理解?有没有因为商业逻辑错导致AI项目失败的案例?", "没有商业思维,只堆AI工具会怎样?比如选错目标客户、定价失衡、交付失控这些具体表现?", "AI创业里,‘运气’到底起多大作用?除了认知和决策,还有哪些不可控但关键的因素?", "为什么说决策能力和运气和AI工具一样重要?在OPC实战中,哪些关键决策点最容易翻车?"], ["网上说一人公司月入十万很轻松,真的靠谱吗?", "那些'一人公司轻松月入十万'的说法是不是夸大其词?", "做OPC创业真能随便月入十万?靠谱吗?", "听说有人靠一人公司月入十万,这现实吗?", "OPC创业收入是不是被网上吹得太神了?"], ["一个人开OPC公司,所有事情都要自己干吗?比如收款、定价这些?", "OPC创业是不是啥都得自己扛,连回款和现金流都要亲力亲为?", "当超级个体户,财务、定价、催款这些活儿都得自己来?", "一人公司是不是从接单到收钱、定价格全得自己操心?", "做OPC会不会特别孤独,而且所有运营事务都得自己搞定?"], ["云南省有没有专门支持AI方向OPC创业的新政策?是哪年发布的?", "2026年云南人社厅出了个AI OPC创业支持文件,具体叫什么名字?", "云南省关于人工智能OPC创业的省级专项政策是哪个部门发的?", "听说云南今年出了个AI OPC扶持政策,是人社厅出的吗?几月份的?", "云南省最新的人工智能OPC创业支持政策,全称和发布时间是什么?"], ["云南现在有没有专门为AI OPC创业者建的生态社区?里面提供啥?", "云南的AI OPC创业生态社区能提供算力、绿电这些资源吗?", "云南打造的AI OPC创业社区是不是包含免费空间、普惠算力和全链条服务?", "云南的AI OPC生态社区怎么申请?有补贴吗?", "云南鼓励用高校场地建AI OPC社区,里面整合了哪些要素?能拿补助吗?"], ["云南OPC创业者能申请免费工位吗?在哪儿申请?", "OPC创业者在云南园区能拎包入住免费工位?需要什么条件?", "云南有没有给OPC人才提供的免租公寓或青年驿站?", "在云南做超级个体户,能住人才公寓或者青年驿站吗?收费吗?", "云南园区OPC专区的免费工位和安居支持(比如低租公寓)具体怎么落实?"], ["云南现在对超级个体户有啥贷款政策?云岭创业贷能贷多少?算力贷和数智创新专项信贷是啥?", "我是个人创业者,想在云南申请信用贷款或抵押贷款,最高能贷多少?有没有专门支持AI创业的贷款产品?", "云南OPC超级个体政策里的金融支持包括哪些?企业法人和个人创业者分别能贷多少?", "云岭创业贷对企业法人和个人创业者额度一样吗?3000万抵押贷是针对企业的还是个人的?", "听说云南有‘算力贷’和‘数智创新专项信贷’,这些贷款怎么申请?需要啥条件?"], ["云南给创业者的补贴有多少钱?一次性创业补贴是5000块吗?电商创业能拿多少?", "开网店算电商创业吗?在云南做电商能领最高3万元补贴?要满足啥条件?", "云南OPC政策里的一次性创业补贴和电商项目补贴能同时拿吗?", "刚注册个体户能申5000块创业补贴吗?电商补贴是按项目还是按人发?", "云南对超级个体户的创业补贴具体咋发?是直接打钱还是报销?电商补贴要提供哪些材料?"], ["云南企业招高校毕业生见习,每月补贴多少钱?留用率超一半真能涨到2000块?", "我们公司接收应届生见习,每人每月能领多少见习补贴?怎么才算留用率达50%以上?", "云南OPC政策里的见习补贴是给企业还是给学生?1500元和2000元的区别到底在哪?", "高校毕业生见习期一般多长?补贴是按月发还是一次性结清?", "吸纳见习人员后,企业怎么申请这个见习补贴?需要签合同吗?"], ["学AI相关技能比如数据标注,在云南培训能拿补贴吗?最多补多少?", "数据标注员培训补贴是7280元?创业培训又是1700元?这两个能一起申吗?", "云南对AI职业的培训补贴覆盖哪些工种?除了数据标注还有哪些算AI相关?", "参加政府认可的AI培训班,补贴是直接发给个人还是给培训机构?", "创业培训补贴和技能培训补贴申请条件一样吗?需要先就业还是可以零基础报名?"], ["云南AI领域的超级个体创始人能评职称吗?人工智能专业职称怎么评?", "没学历、年纪大、没资历,能在云南评AI方向的人才称号吗?兴滇英才计划怎么申报?", "OPC创始人进‘兴滇英才支持计划’有啥好处?人才自主评价机制具体怎么操作?", "人工智能专业职称评审打破学历限制是真的吗?需要发表论文或专利吗?", "云南对AI创业者的省级人才认定有哪些?除了兴滇英才还有别的计划吗?"], ["云南现在办OPC超级个体营业执照,能一次搞定所有手续吗?", "在云南注册OPC超级个体,是不是填一张表、上一个网站就能办完?", "云南的‘个人创业一件事’集成服务具体怎么操作?", "OPC超级个体在云南能不能一网通办、一次办结?", "云南对OPC超级个体有没有政务服务优化措施?"], ["昆明市大学生创业园属于市级还是省级的孵化平台?", "昆明市大学生创业园是哪个层级的创业支持载体?", "想申请昆明市大学生创业园入驻,这个园区归谁管?", "昆明市大学生创业园是不是市级政府主办的?", "云南的OPC政策里提到的市级创业园具体指哪个?"], ["云南OPC超级个体政策面向哪些年轻人?", "在校大学生和毕业几年内的毕业生能申请云南OPC吗?", "留学生包括归国和外籍的,能不能享受云南OPC政策?", "毕业超过5年但没到8年的高校毕业生,还符合云南OPC申请条件吗?", "云南OPC政策里说的‘青年创业者’具体包括哪些人群?"], ["云南OPC超级个体项目必须做哪些行业才算符合要求?", "昆明市‘10+N’产业链都包括哪些方向?比如人工智能算不算?", "做新媒体直播或生物医药的项目,能不能申请云南OPC?", "拿过市级创业比赛奖的项目,在云南OPC入驻时有优先权吗?", "有发明专利或者学术成果,申请云南OPC会不会更容易通过?"], ["云南OPC超级个体能免费用办公场地吗?", "入驻云南OPC有没有工位或办公室提供?", "OPC超级个体在云南能申请免费独立办公区吗?", "云南给OPC创业者提供的是共享工位还是单间?", "云南OPC政策里的空间保障具体是免费提供什么?"], ["云南OPC超级个体政策里,孵化服务具体包括哪些内容?", "创业初期能获得哪些一对一指导?", "有没有商业模式梳理和路演机会?", "云南的OPC孵化服务包含专家辅导吗?", "想参加创业沙龙或项目路演,云南OPC政策支持吗?"], ["云南OPC政策有融资支持吗?具体怎么帮创业者找钱?", "种子资金是政府出的还是创投机构出的?", "每年组织多少场融资对接活动?", "OPC创业者能参加融资路演或对接会吗?", "云南有没有专门针对超级个体的早期投资渠道?"], ["云南OPC政策能帮忙申请哪些创业扶持资金?", "税收减免和社保补贴具体怎么申请?", "政策对接服务包括哪些实际帮扶?", "个人创业者在云南能享受哪些财政补贴?", "OPC个体户可以申请减税或社保补助吗?"], ["云南OPC政策提到资源整合,都对接哪些单位?", "高校、科研院所和大企业怎么跟OPC创业者对接?", "作为个体创业者,能通过OPC链接到龙头企业资源吗?", "云南OPC能帮我联系大学或研究院合作吗?", "政策里的‘资源整合’是指拉通产学研资源吗?"], ["云南OPC的AI育笋计划是免费的吗?都教什么?", "‘AI+OPC’培训包含一人公司注册和成本控制吗?", "有没有面向超级个体的AI应用实操课?", "AI育笋计划和AI+OPC培训是一回事吗?", "免费AI培训覆盖从入门到落地的全流程吗?"], ["云南现在OPC超级个体政策有哪些服务?", "在云南做OPC超级个体,能享受哪些代办和咨询服务?", "云南OPC政策里说的一站式支持都包括啥?工商税务知识产权法律这些都能帮办吗?", "云南OPC超级个体能免费办营业执照和报税吗?", "想在云南搞OPC,工商注册、税务、法务、知产这些事有人全程代办吗?"], ["OPC的核心范式是什么?", "OPC最根本的运作方式或底层逻辑是啥?", "什么叫OPC核心范式?能用大白话解释一下吗?", "OPC和其他创业模式最大的区别在哪?是不是就靠那个核心范式?", "OPC强调的‘核心范式’具体指哪一套方法论或结构?"], ["OPC创业是不是只要1个人加几个AI就能干?创始人和AI各自干啥?", "OPC的1+N模式里,人负责什么、AI又干哪些活?", "听说OPC是‘1个创始人+N个AI员工’,这N个AI到底做哪些重复性工作?", "OPC中创始人是不是只管拍板和创意,其他都甩给AI?", "OPC的1+N模式具体怎么分工?比如写代码、做客服、搞运营这些谁来干?"], ["OPC创业为啥说从资本密集变成认知密集了?", "不融资、不招人,光靠脑子就能创业?OPC真这么轻松?", "OPC强调认知密集型创业,是不是意味着懂行比有钱更重要?", "OPC不需要团队和投资,那靠啥赚钱?是不是全看创始人对行业的理解深度?", "为什么说OPC把创业重心从砸钱变成了拼认知?有没有实际例子?"], ["OPC创业门槛真的变低了吗?现在开公司还需要懂编程会外语吗?", "OPC说门槛从资源降到认知,具体哪些事AI能替我搞定?", "不会写代码、英语不好、没客户资源,还能做OPC吗?AI能帮我补上这些短板吗?", "用Claude写代码、AI生成内容、实时翻译、AI筛选客户——这些在OPC里都是标配吗?", "OPC里提到的AI工具链(比如Claude Code、AI Agent)普通人能直接上手用吗?需要技术基础吗?"], ["OPC目前有哪些已经落地验证的商业模式?", "OPC跑通了哪六种商业路径?", "哪些OPC商业模式已经被实际验证可行?", "园区说的六大已跑通的OPC模式具体指哪些?", "OPC领域里哪些商业模式已经有成功案例了?"], ["专业服务型OPC具体是做什么的?", "靠专业技能接项目、按小时或项目收费的OPC模式叫什么?", "深度定制化咨询、高利润率的OPC创业方式是什么?", "以专业能力为核心、做定制方案的OPC类型怎么称呼?", "OPC里那种靠专家经验卖服务、不卖产品的模式叫啥?"], ["高端服务型OPC适合什么人做?", "一单能收几十万的战略顾问、私教类OPC属于哪种模式?", "靠行业洞察和战略判断力赚钱的OPC类型叫什么?", "OPC中主打高端顾问、私董会、战略规划的是哪一类?", "强调认知高度和决策能力、客单价特别高的OPC模式是哪种?"], ["知识产品型OPC主要卖什么?", "把专业知识做成课程、电子书、会员的OPC模式叫什么?", "边际成本几乎为零、靠标准化内容变现的OPC类型是哪种?", "系统化知识打包成数字产品的OPC创业方式怎么称呼?", "为什么说知识产品型OPC最被看好?它具体怎么做?"], ["数字产品型OPC包括哪些具体形式?", "SaaS、模板、插件这类可复用工具属于哪种OPC模式?", "靠订阅制或API收费的软件类OPC创业方式叫什么?", "能规模化复制、有最大增长潜力的OPC类型是哪一种?", "OPC里做应用、工具、自动化模板的是哪种模式?"], ["OPC创业里,靠写博客、拍视频、做社交媒体赚钱属于哪种模式?", "如果我主要靠个人IP接广告、做知识付费,这算OPC里的什么创业类型?", "创意内容型OPC具体怎么变现?有哪些常见方式?", "做自媒体、短视频、小红书博主,属于OPC五种模式里的哪一种?", "靠影响力产出内容并赚钱,是OPC中的‘创意内容型’吗?"], ["OPC里不自己生产产品、只帮别人牵线搭桥收佣金的模式叫什么?", "做中间人撮合资源、收服务费或佣金,属于OPC的哪种创业方式?", "平台整合型OPC是不是就是当‘二道贩子’或者‘中介’?", "没有自有产品,靠连接供需双方赚钱,这是OPC里的平台整合型吗?", "OPC中强调资源对接和撮合收费的模式具体指什么?"], ["怎么判断OPC哪种创业模式最适合我?", "新手想入局OPC,该怎么选适合自己的创业路径?", "有没有简单的方法帮我决定该走内容型、顾问型还是平台型?", "选择OPC创业模式时需要考虑哪些关键因素?", "如何结合自身情况匹配最合适的OPC模式?"], ["OPC创业前必须搞清楚的三个核心问题是什么?", "卖能力还是卖系统?服务边界清不清楚?执行者还是系统设计师?这三个问题到底在问什么?", "OPC选模式时,为什么要想清楚自己是执行者还是系统设计师?", "‘服务边界清不清楚’在OPC创业中具体指什么?怎么判断?", "OPC里说的‘卖能力’和‘卖系统’有什么区别?举个例子?"], ["OPC中顾问或咨询型适合什么样的人?", "专业强、人脉广的人更适合OPC里的哪种创业模式?", "AI能帮OPC顾问型创业者做哪些事?比如找客户、写方案?", "做咨询类OPC,AI现在能替代哪些重复性工作?", "如果我有多年行业经验但不想天天干活,OPC顾问型+AI辅助靠谱吗?"], ["我想靠写文章、做短视频创业,有什么适合个人IP的轻量级创业方式?", "有没有那种内容能反复用、发多个平台还不怎么花钱的创业方法?", "我擅长总结知识和表达,有什么创业模式特别适合我这种人?", "做知识类自媒体,怎么把内容变成能长期赚钱的生意?", "个人IP创业是不是就是做公众号、小红书、抖音这些?还有没有别的玩法?"], ["怎么把我的咨询经验变成一个能自动卖的产品?", "有没有办法把服务做成像软件那样一次开发、多次销售?", "想从接单做项目转成卖标准化数字产品,该怎么做?", "SaaS或者数字工具这类产品化服务创业,难在哪里?", "把专业方法论打包成模板或工具卖,具体要怎么操作?"], ["现在哪些行业特别适合一个人创业做OPC?", "健康、教育、企业服务这些热门赛道,普通人入局机会大吗?", "做心理支持或IoT硬件方向的一人公司,需要什么基础?", "金融、法律这类专业服务,一个人能干得起来吗?", "机器人、SaaS、知识付费……哪些赛道更适合新手试水OPC?"], ["刚想搞一人公司,是应该先辞职全职干,还是边上班边试试?", "副业验证模式具体指什么?要做多久才算验证成功?", "不辞职直接注册公司行不行?风险大不大?", "为什么说别一上来就辞职做OPC?有没有真实踩坑案例?", "副业阶段该重点测试哪些东西,才能判断值不值得转正职?"], ["个体户和有限公司开一人公司,到底选哪个好?", "刚起步接小活,注册个体户是不是最省事省钱?", "想以后跟企业合作,是不是必须注册有限责任公司?", "一人有限公司怎么避免老板个人财产和公司财产混在一起?", "个体户税少但信用弱,有限公司信用好但麻烦多,怎么平衡?"], ["成立一人公司,是注册有限公司好还是个体户好?", "B端长期合作该选有限公司还是个体户?", "刚创业做产品验证,适合注册个体户还是有限公司?", "一人公司选有限公司和个体户各有什么优缺点?", "面向企业客户长期合作,注册一人有限公司合适吗?"], ["公司起名要准备几个备选名字?", "注册一人公司时,公司名称怎么取才容易通过?", "在‘一网通办’上核名要填哪些信息?", "公司名称格式是不是必须包含行政区划、字号、行业和‘有限公司’?", "市场监管局官网核名需要提交多少个名称备选?"], ["注册一人公司线上要提交哪些材料?", "设立登记申请要准备公司章程、身份证和地址证明吗?", "网上提交一人公司注册申请后多久能拿到营业执照?", "注册一人有限公司,线上办理大概要几天?", "营业执照一般多久能下来?"], ["一人公司刻章要刻哪几枚章?", "公章、财务章、法人章都要刻吗?去哪刻?", "刻公司印章要多少钱?大概费用是多少?", "公安备案的刻章点在哪里找?需要预约吗?", "刻章必须去公安指定地点吗?费用大概500块对吗?"], ["一人公司开基本户要带什么材料?", "银行开户要预约吗?需要法人本人去吗?", "开公司对公账户年费一般是多少?", "营业执照、公章和法人身份证都带齐了就能当场开户吗?", "基本户年费500到1000元,是所有银行都这样吗?"], ["领取营业执照后多久要办税务登记?", "公司注册完要多久去税务局报到?", "办完执照后30天内必须做税务登记吗?", "税务登记要核定哪些税种?", "新公司怎么申请发票?"], ["一个人能当公司法人吗?", "一人公司还需要设监事吗?", "现在一个人开公司,自己当老板还要请别人当监事吗?", "新公司法允许只有一名股东兼法人吗?", "一人有限公司的法定代表人可以是唯一股东吗?"], ["注册公司对注册资本有啥要求?", "注册地址有什么规定?", "公司注册时地址必须是商用吗?", "注册资本和注册地址分别要注意啥?", "注册公司时地址和资金方面有哪些硬性条件?"], ["一人公司注册时要马上把钱打到公司账户吗?", "认缴制下注册资金是不是可以先不交?", "公司章程里要写清楚多久交齐注册资本吗?", "认缴期限最长能写几年?", "5年内必须把认缴的注册资本全部实缴到位吗?"], ["认缴的注册资本没交完会有什么风险?", "注册资本写太高会有什么后果?", "公司亏了,股东要拿自己钱补注册资本吗?", "‘认缴’是不是等于不用缴?", "如果公司经营不善,股东要按认缴金额赔钱吗?"], ["注册一人公司时,住宅地址能当注册地址吗?", "一人有限责任公司可以用家里地址注册吗?", "注册OPC必须用商用地址吗?住宅行不行?", "写字楼、商铺、共享办公这些算商用地址吗?", "虚拟地址和集中登记地属于商用地址吗?"], ["园区提供的共享工位能用来注册公司吗?", "租用共享办公空间可以作为公司注册地址吗?", "集中登记地或虚拟地址要收费吗?大概多少钱?", "有没有免费的虚拟地址?园区会提供吗?", "3000到6000元一年的虚拟地址服务靠谱吗?"], ["公司没业务也要做账报税吗?", "零收入的一人公司需要记账和报税吗?", "不经营的OPC还要请会计或者报税吗?", "记账报税具体包括哪些事情?", "合规要求里提到的‘合规’主要指哪些方面?"], ["一人公司不赚钱也要做账吗?", "零申报是什么意思?怎么操作?", "不做账或漏报税会有什么后果?", "按月报还是按季报?怎么选?", "被列入经营异常名录会影响个人信用吗?"], ["小规模纳税人开普票,季度30万以内真的不用交增值税吗?", "小微企业企业所得税5%的优惠能持续到什么时候?", "年利润300万以下的企业所得税怎么算?", "2027年底前的税收优惠包括哪些?", "开专票也享受30万免征增值税吗?"], ["一人公司分红要交多少税?", "OPC公司分给老板的钱要扣多少个税?", "一人有限责任公司利润分配后要缴20%个税吗?", "股东从一人公司拿分红,个税怎么算?", "一人公司税后利润转到个人账户要交20%税吗?"], ["代账公司一个月收多少钱?", "刚成立的一人公司请代账大概要花多少?", "初创期找代理记账贵不贵?一般多少钱一个月?", "OPC公司起步阶段建议自己做账还是找代账?费用多少?", "代理记账200到500元/月,是按年付还是一月一结?"], ["一人公司年报什么时候交?", "OPC公司每年必须报年报吗?截止日期是几号?", "国家企业信用信息公示系统年报在哪填?几月几号前完成?", "6月30日前没提交年报会怎样?", "一人有限责任公司年报提交入口和时间要求是什么?"], ["一人公司能不能用老板的私人银行卡收公司款?", "公私账户混用会有啥风险?", "为什么说公司钱和老板钱要分开?不分开会担责吗?", "股东个人账户和公司账户混在一起,会被追债吗?", "如何避免一人公司股东对公司债务承担连带责任?"], ["选AI工具是看名气大不大还是看合不合适?", "不同任务该挑哪个AI工具?有没有通用最强的模型?", "为什么不能光挑最新最火的AI用?", "AI工具怎么选才不踩坑?是不是得先想清楚用在啥场景?", "有没有一款AI能搞定所有工作?选工具到底该看啥?"], ["AI在园区里主要用来干啥?", "园区里大家最常用AI来做什么?", "AI在咱们这儿是写代码多还是做内容多?", "为啥说AI在园区首先是编程助手?", "AI在软件开发和内容创作之间,园区更侧重哪个?"], ["选AI工具时最看重什么?", "什么样的AI平台更适合做自动化流程?", "怎么判断一个AI平台能不能接API或者装插件?", "工作流编排是什么意思?哪些AI支持这个功能?", "生态开放的AI工具有哪些特点?"], ["写代码用哪个AI工具好?", "DeepSeek和智谱GLM哪个更适合程序员?", "有没有开源又便宜的编程AI推荐?", "能自己部署的编程AI有哪些?", "DeepSeek和GLM在性价比和私有化方面各有什么优势?"], ["中文场景下用什么AI最合适?", "通义千问适合哪些具体用途?", "阿里云百炼和通义千问是什么关系?", "长上下文对日常使用有啥帮助?", "通义千问在云服务集成方面强不强?"], ["豆包能免费用吗?适合做什么?", "字节的豆包能生成视频和图片吗?", "豆包和通义、DeepSeek比起来有啥不同?", "创意类工作比如做海报、剪视频,用哪个AI好?", "C端免费的多模态AI工具推荐哪个?"], ["Kimi适合处理哪些类型的文档?", "长篇报告或研报用什么AI工具处理比较好?", "有没有能处理超长文本的AI工具推荐?", "处理几十页PDF文档该用哪个AI?", "Kimi和其它大模型比,长文本处理有什么优势?"], ["政企单位用什么AI工具比较合规?", "央国企内部推荐用哪个国产AI?", "需要资质认证的场景该选文心一言还是讯飞星火?", "百度和科大讯飞的AI工具在政务场景下怎么选?", "做政府项目时,语音识别和信创适配哪个AI更合适?"], ["微信里能直接用的AI工具有哪些?", "混元在微信生态里怎么用?", "腾讯混元适合做快速决策还是深度分析?", "有没有支持微信公众号/小程序集成的AI工具?", "混元的‘快慢思考’具体指什么功能?"], ["有没有好上手的AI工作流搭建平台?", "非技术人员怎么自己搭AI流程?", "什么是Agent平台?园区推荐哪个?", "AI自动化流程该用什么工具来设计?", "工作流和智能体(Agent)平台有什么区别?"], ["Coze扣子是哪家公司的产品?", "零代码搭建AI机器人用什么平台?", "扣子支持接入哪些大模型?比如豆包、Kimi这些能用吗?", "个人用户想做个自动问答机器人,用扣子方便吗?", "拖拽式AI Agent平台除了扣子还有别的推荐吗?"], ["Dify适合什么类型的企业用?", "Dify能支持私有化部署吗?", "Dify支持哪些大模型?", "Dify是偏技术还是偏业务的AI工具?", "Dify的工作流节点多不多?"], ["用无代码平台搭智能体要花多少钱?", "做个客服智能体一个月大概多少钱?", "无代码做内容/竞品/线索智能体贵不贵?", "有没有便宜的AI智能体搭建方式?", "170元能搞定哪些AI智能体?"], ["AI智能体和普通聊天机器人有啥区别?", "AI智能体是不是就是高级点的Chatbot?", "什么叫‘闭环系统’的AI智能体?", "AI智能体能自己定目标和改方案吗?", "为什么说AI智能体比对话机器人更智能?"], ["AI智能体由哪几部分组成?", "大模型+记忆+规划+工具调用,这四个缺一不可吗?", "没有记忆或不会调用工具还算AI智能体吗?", "AI智能体的核心公式是什么?", "‘主动规划’在AI智能体里指什么?"], ["AI智能体的三个关键能力是啥?", "智能体怎么实现自主决策?", "AI智能体能自动发邮件或查网页吗?", "它能一边干活一边优化自己的方法吗?", "工具调用具体包括哪些操作?"], ["智能体一般由哪几个核心部分组成?", "一个成熟的AI智能体包含哪些关键模块?", "AI智能体的五大组成部分是什么?", "智能体的‘大脑’指的是什么?它还有哪些配套能力?", "为什么说大模型只是智能体的大脑,还需要其他模块配合?"], ["怎么从零开始搭一个自己的AI智能体?", "普通人能自己动手做一个AI智能体吗?需要哪些步骤?", "搭建智能体的具体操作流程是怎样的?", "有没有适合新手的智能体搭建指南?", "搭建一个可用的AI智能体难不难?要学编程吗?"], ["给智能体设定角色时要注意什么?", "怎么让AI智能体像某个具体岗位的人一样工作?", "上传历史文档算不算角色设定的一部分?", "角色设定和知识边界分别指什么?怎么设置?", "能不能让智能体只懂我公司的业务,不回答外部问题?"], ["什么是RAG?怎么用它给智能体加‘记忆’?", "怎么让AI记住我的笔记、SOP和白皮书?", "文档切片和向量检索在智能体里起什么作用?", "挂载知识库后,智能体是不是就能回答公司内部问题了?", "RAG是不是就是把我的资料喂给AI让它记住?"], ["智能体的工作流设计具体怎么做?", "自媒体运营类智能体是怎么自动完成热点追踪→写文案→做图→分发的?", "怎么把日常重复工作变成智能体可执行的流程?", "工作流设计和普通自动化脚本有啥区别?", "一人公司怎么用工作流设计让一个智能体顶好几个岗位?"], ["AI智能体怎么才能像真人一样有手有脚去干活?", "智能体怎么接入外部工具比如搜索和数据库?", "让AI智能体具备实际操作能力,需要接哪些插件或API?", "AI智能体的‘手脚’具体指什么?怎么实现?", "智能体怎么调用外部服务来执行任务?"], ["怎么防止AI胡说八道乱编答案?", "做AI智能体时最容易踩哪些坑?", "MVP原则在AI智能体开发中怎么落地?", "怎么通过对话日志优化提示词?", "智能体上线后该定期看哪些数据来迭代?"], ["OPC的1+N组织架构到底是什么意思?", "OPC模式里‘1’和‘N’分别代表什么?", "什么是1+N架构?跟传统公司结构有啥不一样?", "OPC组织里核心层和数字员工是怎么分工的?", "1+N架构中的N是固定人数还是可伸缩的?"], ["一人公司老板每天到底该干哪四件事?", "核心层那一个人只做战略、标准、审核和管钱吗?", "定标准和审关键输出具体指哪些工作?", "现金流管理在一人公司里为什么必须由核心层亲自抓?", "一人公司的创始人是不是就只管这四件事,其他全交给AI?"], ["AI数字员工有哪些常见岗位类型?", "一个AI智能体能当销售客服,还能当财务行政吗?", "产品岗和营销岗的AI智能体是不是要分开建?", "流量投放和内容创作能用同一个AI智能体搞定吗?", "交付项目和财务行政这类不同职能,为啥要拆成独立Agent?"], ["AI搞不定的设计、剪辑、翻译这些活儿,具体怎么外包出去?", "哪些工作适合外包给外部团队而不是让AI干?", "为什么设计剪辑翻译这类重活要外包,而不是全靠AI?", "外部协作层主要包哪些活?能举几个实际例子吗?", "一人公司怎么把AI干不了的活儿交给别人做?"], ["什么叫流水线工作流?和运营闭环有啥区别?", "流水线工作流具体指哪几个环节?", "园区说的‘流水线工作流’到底是什么意思?能通俗讲讲吗?", "工作流是按步骤自动跑的吗?需要人工干预吗?", "流水线工作流和日常说的SOP是一回事吗?"], ["一人公司用AI的标准流程是怎样的?从接单到交货走几步?", "AI生成初稿后,创始人具体要审哪些地方?怎么微调?", "成品入库或上线前,AI优化排版都做些什么?", "需求输入之后,AI是怎么一步步拆解和生成的?", "标准工作流里哪几步必须人来把关?哪几步能全自动?"], ["AI客服真能7×24小时回消息还自动生成报价单?靠谱吗?", "AI怎么实现多平台批量发内容?支持哪些平台?", "标准化SOP+AI自动交付,实际是怎么落地的?", "AI记账报税能做到什么程度?需要人复核吗?", "运营闭环包含哪几块?每块AI具体干了啥?"], ["一人公司启动成本真能压到每月500块?都包含哪些开销?", "试错周期缩短到2-6周,是指从想法到验证结果只要这么短时间吗?", "相比传统创业,成本省在哪?500元和5000元档差别大吗?", "为什么AI能大幅压缩启动成本和试错时间?底层逻辑是啥?", "每月500-5000元这个成本范围,覆盖了哪些服务或工具?"], ["国内大模型厂商有哪些?怎么分梯队的?", "豆包、通义千问和DeepSeek是不是国内前三的大模型?", "国内大模型‘三超多强’具体指哪几家?", "字节、阿里、百度、腾讯各自的大模型叫什么名字?", "Kimi和MiniMax属于国内大模型的哪个梯队?"], ["大模型之间差别大吗?", "问答简单问题时,不同大模型效果差多少?", "为啥说复杂任务才看出大模型好坏?", "多模态和多轮对话能力哪家强?", "常识题大家都能答对,那真正比拼的是啥?"], ["DeepSeek适合干啥?写代码行不行?", "DeepSeek开源吗?许可证是啥?", "DeepSeek API贵不贵?性价比怎么样?", "数学和推理任务用DeepSeek好不好?", "有没有开源又便宜、还擅长代码的大模型?"], ["通义千问适合企业用吗?跟阿里云有啥关系?", "通义千问中文理解强不强?长文本处理怎么样?", "阿里云百炼平台支持通义千问吗?", "通义千问回答问题完整度高不高?", "企业上云选大模型,通义千问有啥优势?"], ["豆包能干啥?跟其他大模型比有啥特点?", "豆包免费的多模态功能具体包括哪些?", "豆包适合做智能体(Agent)开发吗?", "豆包在逻辑推理和编程方面表现如何?", "日活很高的豆包,主要面向C端还是B端用户?"], ["智谱GLM适合做编程任务吗?", "GLM模型在政企私有化部署方面表现怎么样?", "智谱的开源大模型支持长上下文吗?", "GLM做长程Agent任务效果如何?", "有没有适合私有化部署、又擅长写代码的国产大模型?"], ["百度文心一言在中文写作方面强不强?", "文心一言支持图片和语音输入吗?", "哪家国产大模型的合规资质最全?", "搜索功能强、又能写公文的国产大模型是哪个?", "文心一言是不是全模态能力最强的国产模型?"], ["Kimi能处理上百页的PDF研报吗?", "月之暗面的Kimi适合分析财报或行业报告吗?", "有没有特别擅长读长文档的国产大模型?", "Kimi在解析超长文本方面比其他国产模型强在哪?", "哪款国产模型看PDF、研报最准最细?"], ["讯飞星火在教育场景用得多吗?", "科大讯飞的大模型语音识别和合成效果怎么样?", "央国企常用哪个国产大模型做信创适配?", "星火在医疗问诊辅助方面有实际落地案例吗?", "中文理解好、还能语音交互的国产模型是哪个?"], ["混元大模型和微信打通了吗?", "腾讯混元支持图文生成和视频理解吗?", "混元有没有‘快思考’和‘慢思考’两种推理模式?", "社交场景下用得最多的国产大模型是哪个?", "哪个国产模型在微信生态里能直接调用AI能力?"], ["写代码商用推荐哪个国产大模型?", "处理超长文档用哪个国内大模型最好?", "有没有免费又好用的国产多模态大模型?", "政企项目落地该选智谱、文心一言还是通义千问?", "做AI创业项目,国内大模型怎么选型?"], ["国内各家大模型价格为什么不好直接比?", "比价时要注意哪些价格口径问题?", "美元计价和人民币计价的大模型服务怎么换算对比?", "缓存命中率会影响大模型使用单价吗?", "上下文长度不同,大模型收费会变吗?"], ["昆明大学生创业园首批入驻了多少个项目?", "目前有多少项目成功入驻园区?", "园区首期覆盖了哪些产业领域?", "人工智能、低空经济这些方向在园区有项目落地吗?", "园区第一批受理的58个项目里,最终入驻率是多少?"], ["这份知识库的数据来源是哪些文件?", "运营数据里的‘官方统计口径’和‘省级绩效累计口径’有啥区别?", "AI平台运行指标能和园区官方统计数据混着用吗?", "知识库里提到的三类数据口径分别指什么?", "《昆明市大学生创业园创业企业(项目)入驻管理规程》在知识库中起什么作用?"], ["昆明市大学生创业园的英文全称是什么?", "这个园区的正式名称叫什么?", "昆明大学生创业园是不是就叫‘昆明市大学生创业园’?", "KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK 对应的中文名是啥?", "园区全称里带不带‘市’字?"], ["云南首家政府办的大学生创业孵化园区是哪个?", "有没有免费给大学生创业用的政府园区?", "昆明有没有专为青年大学生开的创业孵化基地?", "这个园区是政府主办的吗?对大学生收费吗?", "听说有个开放式创业园区专门服务大学生,是真的吗?"], ["这个园区是哪一年成立的?", "昆明市大学生创业园什么时候开始运营的?", "2009年9月成立的创业孵化基地是哪个?", "园区创办至今有多少年了?", "最早一批大学生创业孵化载体是什么时候建的?"], ["昆明大学生创业园归哪个部门管?", "谁在负责管理这个创业孵化园区?", "昆明市人社局是不是这个园区的主管单位?", "园区的上级主管部门是人社局吗?", "这个创业园是昆明市人社局直接办的吗?"], ["昆明大学生创业园具体在哪儿?", "园区地址是昆明市民航路229号吗?", "怎么去昆明市人力资源中心1-2楼的创业园?", "在昆明市民航路附近有个创业孵化基地,是不是就是这个?", "这个园区和昆明市人力资源中心是一个地方吗?"], ["园区大概有多大面积?", "能同时容纳多少个大学生创业团队?", "3000平米的创业空间能入驻多少支队伍?", "听说最多能支持50个团队,是真的吗?", "园区的物理空间和承载能力怎么样?"], ["园区主要发展哪些产业?", "这个创业园重点支持什么类型的创业项目?", "园区的主导产业是数字经济和电子信息吗?", "你们园区聚焦在哪些行业领域?", "创业孵化主要面向数字经济和电子信息企业吗?"], ["园区是什么时候被评为省级创业园的?", "园区获得省级创业孵化载体认定是哪一年?", "2012年是不是园区被认定为省级创业园的时间?", "园区成为省级创业园有多久了?", "省级创业园资质是哪年拿到的?"], ["园区的联系电话是多少?", "我该打哪个电话咨询入园事宜?", "能发一下园区的客服电话和邮箱吗?", "联系园区工作人员用什么方式?", "园区接收咨询的邮箱是714066050@qq.com吗?"], ["园区提供哪些创业服务?", "什么是‘七位一体’创业扶持体系?", "园区说的‘一站式’孵化服务具体包括哪些内容?", "AI赋能是园区服务的一部分吗?", "空间、孵化、融资、政策这些服务都包含在园区服务体系里吗?"], ["现在园区里有多少家企业在运营?", "截至2026年7月,园区入驻企业总数是多少?", "38家是园区当前的实际入驻企业数量吗?", "最新统计的在园企业数是38家吗?", "2026年7月的数据说园区有38家企业入驻,对吗?"], ["园区最多能容纳多少家企业?", "这个园区一共能入驻几家单位?", "园区的总企业容量是多少?", "园区最多可以放几家公司?", "园区的企业承载上限是几个?"], ["园区开园到现在总共花了多少钱运营?", "到目前为止,园区运营投入了多少钱?", "园区累计花了多少运营经费?", "从开园到现在,园区运营资金总共用了多少?", "园区运营至今一共投入了多少万元?"], ["现在园区里的企业一共招了多少人就业?", "入驻企业今年加起来带动了多少人就业?", "园区企业今年提供了多少个就业岗位?", "目前园区企业累计解决了多少人的就业?", "园区企业今年一共吸纳了多少就业人员?"], ["园区企业今年一共赚了多少钱?", "入驻企业今年的生产经营总收入有多少?", "园区企业今年的总营收是多少?", "所有入驻企业今年合计做了多少营业额?", "园区企业今年生产经营总额达到多少万?"], ["园区企业今年一共交了多少税和利润?", "入驻企业今年上缴的税利总额是多少?", "园区企业今年给政府交了多少钱(含税和利润)?", "今年园区企业累计上缴税利总共多少?", "园区企业今年的税利总额有多少钱?"], ["园区开园到现在一共孵化成功并毕业的企业有多少家?", "从开园到现在,有多少家企业已经成功出园了?", "园区累计成功孵化并出园的企业总数是多少?", "截止目前,园区累计有多少家企业顺利毕业?", "开园以来,园区一共送走了多少家孵化成功的企业?"], ["今年园区一共办了多少场活动?", "2026年园区组织了多少次培训?", "今年园区开展了多少场活动,培训了多少人?", "今年园区的活动场次和培训人数分别是多少?", "2026年园区总共培训了多少人次?办了多少场活动?"], ["统计表里哪些数据是累计的、哪些是当月的、哪些是当年的?", "《2026年7月运行情况统计表》中第3栏和第9栏代表什么?第5栏呢?", "园区统计表里的数据怎么区分是累计、当月还是当年的?", "统计表中各栏数据的统计口径分别是什么?", "怎么看懂园区统计表里不同栏位的数据含义?"], ["省级创业孵化载体认定以来,园区一共孵化了多少家企业?", "截至2026年5月,园区累计孵化企业数是多少?", "园区被评上省级孵化载体后,总共孵化了多少家企业?", "按省级绩效口径算,园区累计孵化企业总数是多少?", "截至今年5月,园区在省级认定下累计孵化了多少家企业?"], ["园区到目前为止一共送走多少家团队或企业?", "省级口径下,园区累计出园的创业实体有多少家?", "2026年这一年园区有多少家团队(实体)出园了?", "截至2026年5月,园区累计和本年度的出园数分别是多少?", "园区累计出园企业和今年新出园的企业各有多少家?"], ["园区入驻团队总共申请了多少项发明专利?", "截至2026年5月,园区累计有多少件发明专利?", "今年新申请的发明专利有多少项?", "入驻团队的专利总数是多少?", "园区孵化企业一共拿了多少个发明专利?"], ["园区入驻团队到现在一共赚了多少钱?", "截至2026年5月,入驻企业的总经营收入有多少?", "累计经营收入大概多少个亿?", "园区企业总共营收多少万?", "现在所有入驻团队加起来收入有2.19亿吗?"], ["园区企业一共交了多少税?", "截至2026年5月,累计缴税金额是多少?", "入驻团队总共纳税426.2万元对吗?", "园区孵化企业合计缴了多少税款?", "税收缴纳总额接近430万了吗?"], ["园区目前带动了多少人就业?", "累计带动就业人数有多少?", "今年新增带动就业多少人?", "1529人是包括今年新招的106人吗?", "园区总共帮多少人找到了工作?"], ["园区创业指导专家一共有多少人?", "运营团队有几个人?", "专家团队和运营人员各有多少?", "创业导师队伍规模多大?", "园区有50个专家加3个运营人员对吗?"], ["园区一共办过多少场创业指导服务活动?", "截至2026年5月,园区累计开展了多少场创业指导活动?", "今年(2026年)园区办了多少场创业指导活动?", "创业指导服务活动总共83场,其中今年有几场?", "园区创业指导活动历年累计和本年度分别是多少场?"], ["园区近3年孵化成功的创业企业比例是多少?", "入孵企业的孵化成功率有多少?", "过去三年里,园区孵化成功的比例是95.91%吗?", "孵化成功率95.91%是指最近三年的吗?", "园区近三年的孵化成功率具体是多少?"], ["哪些人可以申请入驻园区?", "在校大学生能申请入驻吗?毕业几年内的毕业生可以?", "留学生包括归国和外籍的都能申请吗?", "不是法定代表人,只是项目负责人能入驻吗?", "如果公司很优质但申请人不是法人,还能放宽入驻吗?"], ["园区重点支持哪些产业方向的创业项目?", "昆明市‘10+N’产业链具体包括哪些?", "做数字经济或生物医药的项目符合入驻产业要求吗?", "绿色食品、文化旅游这些算园区支持的产业吗?", "我的项目属于新能源领域,符不符合园区的产业定位?"], ["入驻园区对企业经营计划和资金有什么要求?", "申请入驻需要提供详细的经营计划书吗?", "启动资金有没有最低金额要求?", "企业管理制度要规范到什么程度?", "有知识产权纠纷或者经营异常记录还能申请入驻吗?"], ["哪些创业项目能优先入驻园区?", "拿过市级以上创业比赛奖的项目是不是更容易进园区?", "有发明专利或者高学历的团队,入驻园区有没有优待?", "学术成果特别突出的创业者,申请园区入驻会加分吗?", "园区对拥有高新技术专利的项目,入驻时有什么特殊政策?"], ["园区一年组织几次入驻申请?", "入驻申请是固定时间还是随时都能提交?", "如果错过了常规申请时间,还能不能补报?", "园区入驻工作每年安排几次?会不会临时加开?", "入驻申请的时间安排是固定的吗?能不能根据情况调整?"], ["申请入驻园区需要准备哪些材料?", "第一次申请园区入驻,要交哪些表格和证明?", "还没注册公司,只有一份创业计划书,能申请入驻吗?", "外籍创业者申请园区入驻,身份证件要提供什么?", "提交入驻申请时,专利证书和学历证这些必须一起交吗?"], ["材料不全的话,园区会怎么处理我的入驻申请?", "初审没通过是因为材料少了,还能补交吗?", "服务机构初审时发现材料缺东西,会告诉我补什么吗?", "提交材料后被告知不齐全,是不是还有机会补正?", "初审阶段材料有问题,是一次性告知还是分批提醒?"], ["入驻评审是怎么进行的?要答辩吗?", "路演答辩具体怎么操作?有没有评分标准?", "评审专家从哪里来?几个人打分?多少分算通过?", "入驻评审总分100分,80分以上才合格,那各项分别占多少分?", "团队实力、项目创新性、市场前景这些在评审里各占多少比重?"], ["公示是在哪个网站上进行的?", "公示完没异议是不是就代表能入驻了?", "公示阶段需要我本人做什么吗?", "公示期是多长时间?如果有人提异议怎么办?", "公示结果在哪里查?人社局官网具体哪个栏目?"], ["签完协议后多久能拿到工位?", "《入驻孵化协议》里都写了哪些关键内容?", "签协议的时候要带什么材料?", "办公场地是随机分配还是可以自己选?", "签了协议但还没拿到工位,算不算正式入驻?"], ["OPC创业空间适合做AI小程序的团队吗?", "做电商直播的项目能申请OPC的独立工位吗?", "文创设计类项目在OPC最多能待几年?", "OPC是直接就能搬进去办公,不用装修吧?", "软件开发和AI应用类项目,OPC和其他区域有啥区别?"], ["还没注册公司,但有个创业点子,能进园区吗?", "大学生创业团队只能进创业苗圃区吗?", "在苗圃区表现好,怎么才能去加速区?", "创业苗圃区给的是不是那种开放大办公室?", "苗圃区最多待两年,之后必须搬走吗?有没有其他出路?"], ["已经注册了公司,但刚成立三个月,能进孵化加速区吗?", "加速区的2年孵化期满了还能续吗?怎么申请延期?", "独立办公空间是带家具和网络的吗?", "加速区对企业有没有营收或人员规模要求?", "总共最多能在加速区待三年,这三年里要每年评估吗?"], ["国际创客区是给哪些人用的?", "留学生创业能进国际创客区吗?", "外籍学生创业有专门的孵化区域吗?", "归国留学生创业有啥特别支持?", "国际创客区的孵化时间是多久?"], ["园区给创业团队提供免费办公场地吗?", "工位和独立办公室都免费吗?", "办公桌椅、网络这些基础设备园区包吗?", "新入驻团队能拿到哪些硬件支持?", "有没有免费的高速网络和文件柜?"], ["创业初期有老师一对一指导吗?", "园区会组织创业相关的活动吗?", "有没有创业沙龙或者路演机会?", "创业讲堂多久办一次?参加要收费吗?", "刚起步的项目能获得哪些辅导服务?"], ["园区能帮忙找投资吗?", "想贷款创业,园区能推荐银行或政策性贷款吗?", "股权融资的事园区管不管?", "有没有对接风投机构的服务?", "融资这块园区具体怎么帮创业者?"], ["园区能帮我们申请创业补贴吗?", "税收减免和社保补贴怎么弄?", "高校毕业生创业有哪些专属政策?", "创业扶持资金申报流程园区会协助吗?", "政策对接服务包括哪些具体内容?"], ["园区能帮我们对接哪些高校和科研资源?", "能不能推荐我们参加春城创业荟这类活动?", "园区怎么帮企业融入昆明的创新创业服务体系?", "入驻后能对接昆明本地龙头企业吗?", "园区有没有和驻昆高校合作的资源?"], ["园区提供哪些免费的AI培训课程?", "有没有教怎么用AI做内容创作的培训?", "想学AI怎么帮公司省钱增效,园区有相关培训吗?", "一个人创业,园区有专门的AI落地指导吗?", "AI公益培训都包括哪些具体内容?"], ["园区能帮忙办营业执照吗?", "税务申报这些事园区能代办吗?", "知识产权方面有问题,园区能给咨询建议吗?", "遇到法律纠纷,园区提供法律援助吗?", "工商、税务、法务、知产这些基础服务园区都包办吗?"], ["园区每年怎么考核入驻企业?", "考核满分多少分?多少分算合格?", "考核主要看哪几个方面?各自占多少分?", "经营发展能力和技术创新能力分别占多少分?", "考核等级分几档?每档的分数线是多少?"], ["企业什么情况下要离开园区?", "孵化成功了是不是就得搬走?", "如果自己不想继续入驻了能主动退出吗?", "什么情况会被园区强制要求退出?", "孵化期满了还没毕业,还能继续待吗?"], ["如果公司连续好几天没人来办公室办公,会不会被园区赶出去?", "场地空着没用又没提前报备,几次会被强制清退?", "什么情况下园区会直接让企业搬走?比如欠钱、违法或者乱转租行不行?", "不服从园区管理到什么程度算严重?会被踢出去吗?", "公司执照被吊销了,是不是必须马上退出园区?"], ["啥叫OPC创业模式?是不是一个人就能开公司?", "AI一人公司到底是什么意思?靠人工智能怎么单干创业?", "做电商直播或者AI应用,一个人能注册公司并在园区起步吗?", "OPC适合哪些行业?搞软件开发或文创设计能行吗?", "轻资产创业是不是就指不用租大办公室、一个人就能干的活?"], ["OPC创业者在园区能租到独立工位吗?最长能待多久?", "园区给一人公司提供共享办公位吗?有没有拎包入住那种?", "孵化期最多两年,是从签合同那天开始算,还是入驻当天起算?", "园区有没有专门教一个人怎么用AI创业的培训课?叫啥名字?", "‘AI一人公司实战课’多久开一次?新人能报名参加吗?"], ["云南第一个AI一人公司基地在哪?什么时候建好的?", "昆明是不是全省首个搞AI个体创业培育的地方?有官方消息吗?", "2026年3月说的云南首个人工智能OPC人才基地,具体在昆明哪个位置?", "这个AI OPC基地是政府牵头还是园区自己建的?", "之前云南没有专门培养AI个体创业者的基地,对吗?"], ["现在园区里到底有多少家企业?38家还是39家?为啥数字不一样?", "《运行情况统计表》和《入驻企业信息表》哪个才是最新最准的名单?", "云南派音、智海银高、廷秀文旅这些公司都在园区里办公吗?", "名单里有‘人工智能机器人大模型训练’这种项目,是真实入驻的企业吗?", "中越、滇缅、仰光客厅这些带国际字样的项目,也是园区的正式入驻单位?"], ["园区里有哪些企业?能按行业分类列出来吗?", "达岸教育、昆明滇油石化这些公司都在园区里吗?它们分别属于什么行业?", "人工智能和跨境电商类的企业在园区里各有多少家?", "园区里做文旅康养和生物医药的企业有哪些?", "云南星瑞航空和昆明云韵体育是不是也在这个园区?它们属于哪个行业类别?"], ["低空经济相关的企业在园区有几家?", "园区里搞现代农业的公司有哪几个?", "绿色环保和新材料方向的企业各有多少家?", "企业服务类的企业在园区有几家?都叫什么名字?", "低空经济、现代农业、绿色环保这些新兴领域在园区的布局情况是怎样的?"], ["AI平台每个月大概消耗多少token?", "平台每天平均用多少token?每秒处理量大概是多少?", "200亿tokens每月这个数据是官方统计的还是内部测算的?", "AI平台的token消耗量是怎么算出来的?有没有日均或实时速率参考?", "6.67亿tokens日均和7700t/s实时速率这两个数字准确吗?来源是哪里?"], ["每次调用工具大概消耗多少token?", "工具调用次数和token消耗之间是怎么换算的?", "15万tokens/次这个平均值是所有工具都一样吗?", "工具调用和token消耗是实时联动的,具体怎么联动的?", "如果调用次数变多了,token消耗会同比例增加吗?"], ["怎么查园区最新的实时运营数据?", "get_park_overview这个工具要怎么调用?在哪里用?", "园区的实时数据以什么为准?是网页显示的还是必须调工具?", "调用get_park_overview能拿到哪些具体指标?", "实时运营数据更新频率是多少?调一次能获取多长时间范围的数据?"], ["我想申请入驻园区,具体要准备哪些材料?流程是怎样的?需要答辩吗?多少分才算通过?", "入驻昆明市大学生创业园要走什么流程?提交材料后多久能知道结果?每年有几次申请机会?", "创业项目怎么才能进园区?是不是要先交计划书和身份证?评审环节是几个人打分?最低多少分能过?", "园区入驻要面试或路演吗?评审专家从哪来?公示是在哪里看?签协议前还要做什么?", "新人怎么申请入园?需要学历证明吗?评审时是线上还是线下?通过标准是80分固定分吗?"], ["园区给创业公司提供哪些免费或补贴类支持?比如办公室、培训、找投资这些有没有?", "在园区创业能享受哪些实际帮助?像办公场地、法律咨询、AI培训这些包不包括?", "听说有‘一站式’服务,具体包含工商、税务、法律哪些内容?AI专项培训是定期开班吗?", "除了免租金,园区还帮企业对接融资、申报政策吗?辅导是免费的吗?", "园区有哪些孵化服务?空间免费是真的吗?政策申报是指帮我们申请政府补贴吗?"], ["现在园区里有多少家在孵企业?总共带动了多少人就业?是按年度算还是累计算?", "2026年7月的数据说有38家企业,那带动就业213人是指这38家今年新招的吗?1529人又是怎么算出来的?", "园区就业数据有两个口径,到底该用哪个?比如写汇报材料时该引用哪个数字?", "‘累计带动就业1529人’是截至什么时候的?经营收入2.19亿是所有企业的总和吗?", "园区目前实际在运营的企业数量是多少?最新就业带动人数怎么看?不同统计口径的区别在哪?"], ["OPC是什么意思?是不是就是一个人开的公司?和普通个体户有啥区别?", "听说园区支持OPC创业模式,这个‘一人公司’具体指什么?需要几个人注册?", "OPC是昆明园区特有的吗?和有限责任公司比,注册和运营上有什么不同?", "想一个人创业,能注册OPC吗?园区对OPC有没有额外扶持?", "‘One Person Company’翻译成中文就是一人公司吗?在园区里怎么申请这种类型?"], ["园区的原始资料和数据都存在哪里?比如入驻企业名单、运行统计表这些文件能公开查吗?", "对外申报或写总结报告时,该参考哪些权威文件?比如Excel表格和PDF宣传册哪个更准?", "‘数据资料/’目录里的几个文件,比如入驻企业信息表和运行情况统计表,最新更新到什么时候?", "品牌设计稿和空间布局图这些视觉材料,能用于对外宣传吗?有没有使用规范?", "园区所有原始材料都在‘数据资料/’文件夹里吗?比如管理规程和宣传册这些,是不是最新定稿版本?"], ["宣传推广可以用这个知识库里的内容吗?", "我能不能拿这里面的信息去做园区宣传?", "知识库里哪些内容适合对外宣传用?", "做推广材料时能直接引用这里的资料吗?", "宣传推广相关的工作可以参考这个知识库吗?"], ["园区的基本情况都在哪查?", "我想了解园区的基本信息,该看哪部分?", "园区基本信息包括哪些内容?在目录里怎么找?", "哪里能找到园区的概况、地址、面积这些基础资料?", "目录里标着‘园区基本信息’的是第几项?"], ["运营绩效的核心指标有哪些?", "省级申报要求的运营绩效指标是哪些?", "申报省级园区时要看哪些关键绩效数据?", "运营绩效指标按省级标准是怎么定的?", "目录里‘运营绩效核心指标(省级申报口径)’具体指什么?"], ["2026年1到7月的运行数据有吗?", "每月的运行统计表在哪找?", "有没有2026年前七个月的月度运行情况汇总?", "运行情况的月度统计更新到几月份了?", "目录里说的‘2026年1—7月运行情况月统计’包含哪些内容?"], ["入驻企业名单在哪里看?", "一共有多少家企业入驻园区?", "能查到所有入驻企业的详细信息吗?", "39家入驻企业的信息表怎么获取?", "目录里‘入驻企业完整名录(39家/信息表)’包含联系方式和业务范围吗?"], ["孵化加速区在哪儿?", "加速1到10号房间属于哪个区域?", "园区里叫‘孵化加速区’的地方是哪一块?", "我想去孵化加速区,怎么找?", "加速1–10这些办公室是不是都在孵化加速区?"], ["国际创客区具体在什么位置?", "国际1到7号房间属于哪个功能区?", "园区里有国际创客区吗?在哪?", "国际创客区是不是专门给外国创业者用的?", "国际1–7这些工位是在国际创客区吗?"], ["成长区包括哪些房间号?", "成长1到8号房间归哪个区域管?", "园区的成长区在几号楼?怎么走?", "创业公司发展到一定阶段能进成长区吗?", "成长1–8这些空间是不是叫成长区?"], ["OPC创业空间是独立办公区吗?", "OPC创业空间有没有固定房间号?", "园区里提到的OPC创业空间是指哪个地方?", "OPC创业空间和别的区域比如孵化加速区是一样的吗?", "OPC创业空间是对外开放申请的吗?"], ["一楼直播间在园区哪一层?", "园区的一楼直播间具体在哪个位置?", "想用直播间得去一楼吗?", "E区就是一楼直播间对吧?", "一楼直播间是编号E吗?开放给谁用?"], ["园区里有哪些已经成功入驻的省级申报企业?", "能给我看看省级申报的31家入驻实体名单吗?", "哪些企业是通过省级申报进园区的?", "省级申报入驻的企业一共有多少家?", "有没有省级申报入驻企业的详细名单?"], ["企业入驻园区需要遵守哪些管理规定?", "园区对企业(项目)入驻有哪些管理要求?", "入驻后园区怎么管理企业和项目?", "企业入驻管理规程主要讲了哪些内容?", "园区对企业入驻后的日常管理有哪些要点?"], ["园区的整体空间是怎么规划的?", "园区内部的空间布局是怎样的?", "能介绍一下园区各功能区域的分布情况吗?", "园区有哪些主要的功能分区?", "园区的物理空间结构或场地划分是怎样的?"], ["园区宣传册里都写了些什么内容?", "宣传册里关于园区简介、服务和政策的部分具体包括哪些?", "想快速了解园区,宣传册的核心信息有哪些?", "园区宣传册重点介绍了哪些方面?", "宣传册里关于服务和扶持政策的内容有哪些?"], ["企业要满足什么条件才能入驻园区?", "新企业入驻园区的具体流程是怎样的?", "从申请到正式入驻园区,一共要走哪些步骤?", "入驻园区需要符合哪些硬性条件?", "入园的门槛和办理流程分别是什么?"], ["孵化周期是多久?", "园区有哪几个孵化区域?", "四大孵化区域分别叫什么名字?", "不同孵化区域的周期一样吗?", "孵化时间最长能有多久?"], ["怎么报名入驻园区?", "报名需要走什么流程?", "申请入园要提交哪些材料?", "线上还是线下报名?", "报名入口在哪里?"], ["园区上过哪些媒体报道?", "有哪些媒体和园区合作过?", "园区被哪些新闻平台报道过?", "合作媒体名单能看看吗?", "有没有主流媒体对园区的采访或专题?"], ["园区最近有什么新闻?", "园区举办过哪些活动?", "有没有园区的年度大事记?", "想了解园区近期动态,从哪看?", "园区过去一年都做了哪些重要事情?"], ["园区的品牌设计规范在哪查?", "VI手册包含哪些内容?", "Logo、字体、颜色这些怎么用才合规?", "品牌视觉识别标准有官方文件吗?", "对外宣传时VI使用要注意什么?"], ["昆明市大学生创业园是省级的吗?什么时候评上的?", "这个园区叫什么名字?在昆明哪个位置啊?", "园区是哪年成立的?主管部门是谁?谁在负责运营?", "听说2026年4月园区焕新亮相了,是真的吗?现在归谁管?", "园区的运营公司叫啥?统一社会信用代码是多少?申报单位负责人电话能给一下吗?"], ["园区主要支持哪些行业的创业项目?", "整个园区有多大?办公场地和孵化场所各占多少面积?", "园区总共投了多少钱?现在资产值多少?最多能容纳多少个创业团队?", "为什么有的地方写能容纳50户,有的又说49个?哪个数准?", "园区拿过哪些国家级或省级的牌子?比如国家众创空间、科技企业孵化器这些算不算?"], ["园区简介里说在民航路301号,但地址又写229号,到底哪个对?", "园区给大学生创业提供哪些免费服务?除了场地还有啥?", "园区的服务理念和办园宗旨是怎么说的?", "到2026年5月为止,一共孵化了多少家企业?现在还在孵的有多少?", "园区带动了多少人就业?帮企业拿到了多少创业扶持资金?"], ["目前园区实际入驻了多少家创业团队?今年新进了几家?", "应届毕业生(含离校2年内)在园区创业的有多少家?今年新增了几家?", "园区里的创业团队一共拿了几个发明专利?今年新申请了多少项?", "入驻团队去年一共赚了多少钱?交了多少税?今年缴了多少?", "园区创业指导专家有多少人?今年组织了多少场创业指导活动?"], ["园区孵化成功率和出园率分别是多少?数据是近3年的吗?", "现在孵化场所的使用率有多高?90%是指什么?", "返乡农民工能在园区创业吗?有没有相关扶持?", "园区累计孵化181家,但表格里‘团队出园数’写150户,差的那31家去哪了?", "运营团队现在有几个人?是不是就3个工作人员管整个园区?"], ["昆明市大学生创业园的运行情况统计表是哪份文件?", "2026年7月的昆明创业园数据出自哪个表格?", "《昆明市创业平台运行情况月统计表》和《年度汇总表》是不是同一份材料?", "这份园区数据是来自Excel文件还是其他格式?文件名是什么?", "‘昆明市大学生创业园运行情况统计表(2026年7月).xls’这个文件包含哪些内容?"], ["昆明大学生创业园现在有多大?建筑面积是多少?", "园区总共投了多少钱运营?累计花了多少?", "园区最多能容纳多少家企业?现在实际有多少家在园里?", "这些企业一共招了多少人就业?总共赚了多少钱、交了多少税?", "开园到现在一共孵化成功、毕业出园的企业有多少家?今年办了多少场活动、培训了多少人?"], ["报表里哪些数字是‘从开园到现在’一直累计的?哪些是‘今年1到7月’的?", "‘实际入驻企业数’是当月的还是全年的?怎么区分累计值和当期值?", "表格第3栏和第9栏为什么标着‘累计’,其他栏又说是‘当年累计’?有啥区别?", "‘报送当月实际入驻企业数’具体指哪一栏?怎么查这个月新进或退出的企业?", "为什么有些指标写‘当年累计’,有些写‘开园以来累计’?怎么看懂这个统计口径?"], ["园区4月份搬去民航路229号后,面积、容量和资金都变了,具体怎么变的?", "从1月到7月,入驻企业数、带动就业人数这些数据是怎么一步步增长的?", "3月到4月之间园区发生了什么变化?为什么可容纳数突然从18变成49?", "7月底的数据和年初相比,哪些指标增长最多?比如就业人数、营收、活动场次?", "年度汇总表里每月的数值变化能反映园区发展节奏吗?比如哪个月扩张最快?"], ["园区负责人和填表人是谁?联系方式能发我一下吗?", "想联系园区对接工作,找李云鹏还是李禹贤?电话和邮箱是多少?", "填表时间是2026年7月31日,这个数据是最新的吗?还有效吗?", "单位负责人李云鹏和填表人李禹贤是什么关系?分别负责什么?", "714066050@qq.com这个邮箱能用来咨询入园政策吗?联系电话18087174891是24小时开通的吗?"], ["昆明大学生创业园有哪些分区?", "创业园的场地安排分哪几个区?", "孵化加速区、国际创客区这些分区是怎么划分的?", "OPC创业空间和一楼直播间是不是园区的正式分区?", "园区里说的‘加速’‘国际’‘成长’‘OPC’‘一楼直播间’分别指什么区域?"], ["孵化加速区一共有几家入驻企业?", "加速1到加速8,还有加速10,为什么没有加速9?", "孵化加速区总共多少家企业?有没有漏掉编号?", "加速区的企业数量是9家吗?都包括哪些编号?", "创业园孵化加速区包含哪些编号的企业?"], ["加速1公司叫什么名字?负责人是谁?毕业学校和专业是什么?", "云南派音人工智能科技有限公司的创始人王思松是哪个学校毕业的?", "派音科技什么时候启动和注册的?招了几个人?", "派音科技做的是什么业务?他们的Pine模型系列有什么功能?", "加速1公司主要研究什么技术?音频识别、音效生成这些是不是他们做的?"], ["加速2项目叫什么?负责人王昱博是哪所学校毕业的?", "米勒克尔蓝宝石珠宝项目是海归做的吗?学的是什么专业?", "这个蓝宝石项目具体做哪些事?晶体生长和珠宝设计都包括吗?", "蓝宝石新材料和珠宝产业怎么融合?跟上下游怎么合作?", "加速2项目主打高附加值蓝宝石珠宝,是真的做实物珠宝还是只做材料?"], ["加速3项目名称是什么?负责人刘泽位在读什么博士?", "中泰研学合作项目和泰国西那瓦大学有什么关系?", "这个项目提供出国留学服务吗?还做校企合作和企业出海吗?", "刘泽位会说泰语吗?他在侨联有职务吗?", "中泰研学合作项目覆盖哪些服务?比如中外合作办学、产教融合这些包不包括?"], ["云南宸中低空经济有限公司是干啥的?负责人是谁?什么时候成立的?", "王爱玲是哪家公司的负责人?她现在在读什么学校?这家公司主要做低空经济哪些业务?", "园区里新成立的低空经济公司叫什么名字?成立时间、负责人背景和业务方向能说说吗?", "宸中低空经济是不是2026年2月刚成立的?它在园区享受哪些创业扶持政策?", "云南有没有专注低空经济全产业链的初创企业?叫什么?谁负责?有什么特色?"], ["昆明智海银高文化科技有限公司是做什么的?负责人宋芃燃是哪个学校的?", "这家公司做AI财税服务吗?都提供哪些具体服务,比如代理记账、出海咨询这些有吗?", "智海银高是2026年4月成立的对吧?它用AIGC技术做了个什么平台?", "宋芃燃是中共党员吗?她在昆明文理学院学的是审计学?公司主要服务对象是哪些企业?", "有没有做数字化财税+企业综合咨询的公司?叫什么?覆盖海外财税和财务培训吗?"], ["人工智能机器人大模型训练项目是哪个团队做的?负责人周凯什么学历背景?", "这个大模型训练项目是2026年4月启动的吗?具体做数据采集、标注还是模型优化?", "周凯是天津大学毕业的?他带队做的机器人项目主要解决哪些落地场景问题?", "园区里有没有专门做机器人大模型‘训练-优化-应用’全流程的技术项目?叫什么?", "大模型训练项目包括数据清洗和迭代优化吗?是否面向多行业场景落地?负责人是谁?"], ["云南廷秀文旅康养服务项目什么时候开始运营?负责人李雨桐是哪国留学回来的?", "李雨桐在日本读的老年社会学硕士?她回国后要做的康养项目主要解决民宿和旅居点哪些问题?", "廷秀项目针对银发人群,具体怎么提升服务标准、适老能力和运营转化率?", "云南现在推‘旅居云南’,有没有专门做老年文旅旅居服务支持体系的创业项目?", "这个康养服务项目5月启动吗?聚焦旅居空间、民宿和康养点位的服务短板,对吗?"], ["瀚颖AI+教育信息咨询项目是哪家机构发起的?负责人王翰啸是哪个学校毕业的?", "这个项目是不是2026年4月成立的?它搞AI+教育,具体做课程研发、师资共建还是企业赋能?", "王翰啸是云南大学滇池学院毕业的?他们打造的数字教育生态包含哪些环节?", "有没有融合AI技术和教育教学体系的教育服务项目?在昆明园区叫什么名字?", "瀚颖的AI+教育项目提供人才培育和企业赋能服务吗?属于全链条数字化教育生态吗?"], ["云南大学AI+联合创业服务平台是干什么的?", "顾耘华老师负责的那个AI创业平台主要做哪些AI方向?比如AI短视频、AI漫剧这些有具体案例吗?", "云南大学的AI创业平台帮大学生创业成功过吗?有没有拿国家级奖项的项目?", "育笋计划是什么?是不是云南AIGC产业联盟搞的?覆盖了多少大学生?", "王升雅、王洋、杨沐晨、王润曦他们各自做了什么AI相关项目?分别有什么成果?"], ["国际创客区一共有几家?都叫什么名字?", "园区里的国际1到国际7是哪几个国家的团队在做?", "国际创客区的位置在哪?里面都是外国留学生开的公司吗?", "国际创客区总共多少个空间?每个空间对应一个项目吗?", "国际1到国际7是按什么顺序编号的?有没有空着没入驻的?"], ["仰光客厅是谁创办的?是不是缅甸留学生做的?", "仰光客厅这个项目主要是面向谁的?中国游客、在缅中资企业还是缅甸本地年轻人?", "仰光客厅除了住宿和吃饭,还能体验什么缅甸文化?跟当地手艺人怎么合作的?", "仰光客厅是不是在缅甸仰光郊区搞了个沉浸式营地?具体做什么活动?", "仰光客厅算不算中缅文化交流平台?有没有实际带动当地人就业增收?"], ["千楣之路卖的是不是缅甸传统护肤品檀娜卡?", "檀娜卡是用什么做的?听说缅甸2025年要申非遗,是真的吗?", "千楣之路的产品分哪几类?原木套装、粉泥和现代美妆线都有吗?", "千楣之路怎么把缅甸檀娜卡卖到中国?靠小红书还是跨境电商?", "千楣之路是哪个学校的缅甸留学生做的?是不是云大工商管理学院的?"], ["联翼航空技术这个项目是做什么低空经济业务的?", "杨俊蓝是哪个学校毕业的?现在在哪儿读研?项目什么时候启动的?", "联翼航空技术主要面向东南亚哪些国家拓展?做无人机应用还是数据服务?", "低空经济培训是教外国人飞无人机吗?还是也教国内学员?", "联翼航空技术怎么利用云南的区位优势做国际化?有没有具体输出到东南亚的案例?"], ["酷享野农AI农业这个项目是哪个学校的学生搞的?负责人叫啥名字?", "晏和波是谁?他在西南林业大学读什么专业?", "酷享野农AI农业项目什么时候开始的?主要干些啥?比如用AI做农业宣传吗?", "这个农业项目是不是在种高原早熟苹果?跟科研机构和地方政府怎么合作的?", "智慧农业、农旅融合、科研种植这些词具体指啥?酷享野农项目怎么把它们结合起来?"], ["滇缅国际设计是做什么的?是不是专门帮云南和缅甸那边做工程设计?", "张浩鹏是哪个学校毕业的?学的什么专业?", "这个设计公司啥时候成立的?一带一路跟他们有啥关系?", "他们说的跨国协同设计模式是啥意思?绿色建造技术用在哪些项目上?", "交通和城乡基建类的设计,他们有没有跟国外机构一起落地过实际项目?"], ["昆明舒诺生物科技是干啥的?是不是做宠物食品或者营养品的?", "舒岐涛是哪个学校的学生?学的什么专业?", "这家公司什么时候成立的?主打‘精准营养’‘全生命周期养护’具体指什么?", "清洁标签、生物活性技术这些词听起来挺专业的,他们产品到底有啥特别?", "舒诺生物的产品是不是针对猫狗不同年龄阶段做的?配方科学在哪?"], ["达岸教育是做德国留学服务的吗?能办双元制吗?", "吕琳是在泰国读的MBA?她现在负责达岸教育的哪些业务?", "德国双元制留学包括德语培训、签证、境外陪护这些服务吗?", "达岸教育从哪年开始做这事儿的?跟哪些德国学校有合作?", "通过他们去德国留学能不能边学边赚工资?最后能拿到学历和工作机会吗?"], ["成长区一共有几家创业公司?编号是从成长1到成长8吗?", "成长区和其他区域(比如国际4、5)有啥区别?是按发展阶段分的吗?", "成长1到成长8这些项目都在同一个地方办公吗?属于园区什么类型的企业?", "成长区的企业是不是都处于初创或快速成长阶段?有没有统一的支持政策?", "我想了解成长区某一家公司,比如成长3,该怎么查它的详细信息?"], ["花仙子园艺肥料公司是哪个学校毕业的人创办的?创始人叫什么名字?", "这家做家庭园艺肥的公司是哪年成立的?主要解决养花人哪些实际问题?", "花仙子肥料有没有异味?产品是不是容易操作?现在是批量生产了吗?", "翁淞耀是云南师大哪个专业的?他做的肥料线上线下都怎么卖?", "花仙子公司服务对象主要是谁?比如阳台种菜、家里养花的人能用吗?"], ["研X平台是给哪些人用的?说的‘1621万硕博’和‘6000万泛科研人群’具体指什么?", "同行者网络这个APP有苹果和安卓版吗?高校老师有没有投资?投了多少?", "‘1+6+N’生态里的6大模块都包括啥?比如代码共享、找工作、谈恋爱这些真都有?", "研X平台现在有多少学校在用?是学生自己注册还是学校统一组织?", "陈品祥是哪个学校毕业的?现在还在教书吗?在哪儿任教?"], ["朵哈玫瑰项目是谁发起的?大学生创业能做玫瑰米酿吗?", "八街的重瓣红玫瑰是地理标志产品吗?他们跟农业龙头企业怎么合作的?", "玫瑰米酿是饮料还是酒?生产是标准化的吗?品质怎么保证?", "刘志贤是哪个学校哪个年级的学生?项目是2026年4月才开始的吗?", "朵哈项目只做玫瑰米酿,还是也会开发其他玫瑰相关产品?"], ["舒护安养老公司是做什么护理服务的?比如陪病人去医院或者上门照看老人行不行?", "严宇佳是学广播电视编导的,怎么转行做医疗护理?公司是个人开的吗?", "这家公司才孵化一个月就招了3个人?主要服务哪些病人群体?比如术后、偏瘫、慢病老人?", "舒护安跟医院有合作吗?是直接对接医院安排陪护,还是自己接单?", "居家上门护理包括生活监护吗?比如帮独居老人买菜、提醒吃药这些算不算?"], ["南菌优培项目什么时候正式注册公司?负责人王荣飞是学医的,怎么搞食用菌的?", "他们研发的生物制剂加了海藻糖-6-磷酸和寡糖?这些成分是天然的吗?", "硬皮地星和硬皮马勃是云南特有的野生菌吗?高值化全链开发具体指哪几个环节?", "团队里有分析化学博士和农业硕士,这个项目已经有三年研发基础了?", "南菌优培是专注做生物制剂,还是也做野生菌的种植、加工、销售整条链?"], ["丽裳文化艺术工作室是干啥的?主要做传统文化和茶文化活动策划吗?", "王娟老师开的丽裳工作室什么时候成立的?是不是专门帮云南特产比如茶和干花出海?", "五华区丽裳文化艺术工作室的负责人是谁?她学的是幼教专业,怎么转行做国风文旅策划了?", "丽裳工作室说搞‘账号引流+内容传播+产品出海’,具体是怎么操作的?有做抖音或小红书推广吗?", "这个工作室是不是靠云南自贸区政策优势,帮民俗文创和高原特色产品卖到国外?"], ["星瑞航空这个项目是真搞飞机还是只用无人机?他们用的是大疆哪款行业机?", "尹永浩团队能做文物数字化保护吗?除了拍照片建模,还做三维重建和数字孪生吗?", "云南星瑞航空接不接电力巡检、光伏板检测或者森林防火巡查这类活儿?夜间红外搜救也行吗?", "他们除了飞无人机,还卖设备、处理数据、培训飞手吗?是一条龙服务吗?", "星瑞航空是2026年4月刚启动的?负责人尹永浩是学数字媒体技术的,怎么切入低空智能应用领域的?"], ["那个云南直播私域服务平台叫啥名?是不是专门帮本地商家做抖音直播和私域引流的?", "金满满刚毕业就创业做直播平台?她学的是学前教育,怎么转行搞短视频创作和全域运营?", "这个平台说打造自主IP形象,还建全国性私域流量池,具体指微信社群、小程序还是APP?", "他们真在带大学生创业孵化?有没有实习、培训或者人才推荐机制?", "综合性云南直播私域服务平台是不是主要挖云南特产、非遗、旅游这些本土资源来拍短视频和开直播?"], ["OPC创业空间在哪?里面总共安排了几家单位?都叫D开头的吗?", "D. OPC创业空间是不是就是指那6家入驻企业共用的办公场地?名字里的OPC是缩写吗?", "OPC创业空间里都有哪些项目?是不是包括中外研学、直播平台这些?", "想租OPC的空间办公,得找谁申请?有没有工位或独立办公室可选?", "OPC创业空间和园区其他孵化场地有啥不一样?是不是专供成长系列项目用的?"], ["中外青少年研学项目跟哪些国家合作?英美澳新加这些是不是都有直签学校?", "王得荣老师有剑桥交流经历,这个研学项目是不是主打高端国际游学?教师和家长也能报名吗?", "这个研学服务是一站式搞定签证、课程、住宿和保险,还是只负责对接海外机构?", "他们跟国外教育机构是直营合作,不是通过中介?有没有合作院校名单可以看?", "D1项目除了青少年游学,还做在职教师海外研修或亲子家庭出境教育旅行吗?"], ["星杭商务信息咨询昆明有限责任公司是干啥的?负责人是谁?学什么专业的?", "D2那家公司主要做哪些业务?杨之华是负责人吗?她是不是云南师范大学毕业的?", "杨之华创办的星杭商务咨询公司提供培训和商务接待服务吗?她什么时候毕业的?", "星杭商务这家公司是教育学背景的人开的吗?都做哪些商务服务和信息咨询?", "D2园区企业‘星杭商务’的负责人学历和主营业务分别是什么?"], ["蓝智科技是做什么的?徐安厅是负责人吗?他哪年毕业?", "D3蓝智科技项目是不是搞农业物联网的?专门服务蓝莓种植?有自研监控系统吗?", "蓝智科技的智慧种植系统能监测田间环境和水肥灌溉吗?团队几个人?什么时候启动的?", "徐安厅带的蓝智科技是不是西南地区做蓝莓数字化种植的?他们自己开发了全系列监控系统?", "蓝智科技这个OPC公司是2026年5月开始做的吗?专注农业物联网和智慧基地运营?"], ["云品出滇·纸享万家是卖抽纸的吗?王帅是负责人?哪个学校毕业的?", "D4这个项目是不是用AI工具做电商抽纸品牌?现在在拼多多卖得怎么样?", "‘纸享万家’是不是主打悬挂式抽纸?未来要往东南亚做跨境电商?", "王帅的云品出滇项目是2026年3月成立的吗?有没有真实销售验证?", "云品出滇这个日用纸品品牌靠AI和电商平台双轮驱动?核心产品是新型悬挂抽纸?"], ["云迹轻旅是做什么文旅服务的?王海璇有十年经验吗?是不是云南大学MBA毕业的?", "D5云迹轻旅工作室提供私人定制旅行和非遗研学小团吗?也帮本地商家做文旅策划?", "云迹轻旅是不是‘内容+服务+技术’三块都做?包括自媒体IP孵化和AI数字文旅服务?", "王海璇的轻旅工作室是单人运营的吗?面向游客和本地文旅商家都服务?", "云迹轻旅有没有做AI相关的文旅技术服务?还做商业咨询和小众旅行定制?"], ["启元人工智能科技是教小孩用AI的吗?李瑞康是负责人?学什么专业的?", "D6启元科技是不是专做8到12岁儿童AI科普的?有自己研发的工具包吗?", "启元人工智能科技在共享空间办AI绘画、AI音乐这些少儿体验沙龙吗?", "李瑞康的公司是不是一人OPC?专注儿童AI工具推广和实操体验?", "启元科技的AI实操工具包是为低龄儿童设计的吗?开展过AI视频制作这类活动?"], ["云南派音人工智能科技、昆明舒护安养老服务、云南大学AI+联合创业服务平台这几家为什么在简介里写的是OPC一人公司,但场地安排却不在OPC分区?", "云南大学AI+联合创业服务平台属于哪个分区?它的详细介绍在哪能查到?", "简介里说的OPC一人公司项目有哪些被分到其他分区了?具体是哪几家?", "云南大学AI+联合创业服务平台是不是在加速10区?它和OPC一人公司的关系是什么?", "‘OPC一人公司’这个说法在简介里出现,但实际场地没分在OPC区,那这些项目到底算不算OPC?比如云南派音、舒护安这些?"], ["一楼直播间总共安排了几家企业?只有1家吗?", "园区一楼的直播间是不是只有一家公司用?是哪家?", "一楼那个直播间是给谁用的?整个一层就一个直播间对吧?", "园区里标着‘E. 一楼直播间’的地方,到底入驻了几家团队?", "一楼直播间这个分区是不是只有一个名额?有没有可能后续加企业?"], ["昆明屿澈电子商务有限公司是做什么的?他们直播卖什么产品?", "昆明屿澈这家公司是不是毕业生创业的?负责人是谁、什么学校毕业的?", "昆明屿澈做的是TikTok Shop跨境直播,主要面向哪些国家?卖什么类型的商品?", "昆明屿澈用AI数字人做24小时直播,还用1688一件代发,这种模式具体是怎么运作的?", "杨君武创办的昆明屿澈,是不是高校毕业生做的东南亚跨境电商项目?用的是AI短视频引流吗?"], ["信息表里‘场地安排’那一栏为空的企业有哪些?一共几家?", "《入驻企业信息表》中没写分区的公司都有谁?比如上古书院是不是其中之一?", "上古书院(云南上古绝学文化发展有限公司)为什么在信息表里没填场地安排?它分到哪个区了?", "园区里有8家企业在信息表里‘场地安排’列是空的,除了上古书院还有哪些?", "‘场地安排’列为空的项目是不是都没分配物理空间?还是只是表格漏填了?上古书院现在实际在哪办公?"], ["入驻企业完整名录一共有多少家?是39家吗?", "39家入驻企业名单在哪能看到?是不是叫‘入驻企业完整名录’?", "赵泳淇负责的是哪家企业?她是不是39家中某一家的负责人?", "赵泳淇是贵州大学毕业的教育技术学专业学生,她创办或负责的企业在39家名录里吗?", "39家企业的负责人信息都公开吗?比如赵泳淇的毕业院校和专业这些细节是从哪来的?"], ["园区里有哪些做国学和家文化传承的公司?", "有没有专门教家训家规或者搞家族文化研学的机构?", "哪家企业是以‘修身齐家’为理念、做国学课程和文化体验的?", "园区里做上古家学文化、能帮定制家训家规的公司叫什么?", "我想找一家把文化体验和家族传承结合起来的国学机构,园区有吗?"], ["中越生物医疗这个项目具体是做什么的?", "园区里有个叫‘中越生物医疗’的项目,它主要干啥?", "中越生物医疗是企业还是合作项目?属于哪个领域?", "听说园区有中越生物医疗项目,它跟生物医药有关吗?", "中越生物医疗项目是哪个单位在运营?涉及哪些技术方向?"], ["园区入驻企业里有越南籍负责人吗?叫什么名字?", "武妙英是谁?她在园区哪个公司任职?", "云南大学越南留学生武妙英,在园区负责哪家企业?", "会四门语言(中英越法)的越南籍负责人在园区哪家企业?", "谁是园区里那个越南籍、在云大读工商管理、懂四国语言的企业负责人?"], ["中越生物医疗项目做的蛋白生产有什么特别之处?", "这个中越联合实验室生产的生物蛋白用在哪些地方?", "中越生物医疗项目已经拿到越南哪些认证了?", "他们说覆盖越南80%以上市场缺口,具体是哪类蛋白产品?", "中越生物医疗的蛋白生产怎么做到合规跨境出口的?满足疫苗和试剂需求吗?"], ["园区有没有做中越之间旅游或文化交流的项目?", "中越文旅交流项目具体开展哪些活动?", "想了解中越两国在文化旅游方面的合作,园区有相关项目吗?", "中越文旅交流是企业还是政府合作项目?主要面向什么人群?", "园区里叫‘中越文旅交流’的项目,和旅游、非遗、语言学习有关吗?"], ["阮胡竹珠是谁?她在园区哪个企业工作?她是云南民族大学哪个专业的学生?她什么时候毕业?", "园区里叫阮胡竹珠的负责人是哪一家企业的?她的学校和专业是什么?", "入驻企业名单里有位叫阮胡竹珠的同学,能查到她的详细信息吗?比如学校、专业、毕业时间?", "云南民族大学广播电视专业的在读生阮胡竹珠,在园区负责哪家企业?", "阮胡竹珠是2027届毕业生?她在园区具体做什么?"], ["园区里有没有做中越旅游交流的公司?主要提供哪些服务?", "哪家企业专门搞中越文化旅游合作?比如旅游定制、语言服务这些?", "做中越旅游宣传和新媒体运营的是哪家入驻企业?有双语能力吗?", "有没有企业既懂越南语又会做旅游短视频和活动策划的?", "搭建中越文旅平台的企业,具体能帮游客做什么?比如订行程、体验文化这些?"], ["‘绿态’环保科技项目是哪家企业?它主要做什么业务?", "园区里叫‘绿态’的项目是干什么的?属于哪个团队?", "‘绿态’这个名称对应的是哪个入驻企业?有什么特色?", "有没有叫‘绿态’的环保类创业项目在园区?", "‘绿态’环保科技项目是独立企业还是某个团队的项目名称?"], ["陈国旗是哪家企业负责人?他是留学生吗?负责什么工作?", "园区入驻企业里有位叫陈国旗的越南留学生,他在做什么业务?", "负责中越供应链和中国分销的那位越南留学生是谁?属于哪家公司?", "陈国旗在园区管海外工厂对接和国内销售渠道,他具体负责哪家企业?", "哪位入驻企业负责人同时管越南工厂和中国全渠道销售?是不是叫陈国旗?"], ["‘绿态’项目做的环保餐具原料是什么?是不是用槟榔叶做的?", "天然槟榔叶餐具是哪家企业研发的?在越南生产、中国销售吗?", "禁塑令背景下做环保餐具的企业有哪些?怎么卖到中国的?有抖音直播吗?", "‘绿态’的餐具在越南哪里生产?怎么运到昆明?在园区有仓库和直播间吗?", "用mo cau(槟榔叶)做的可降解餐具,是哪个园区企业做的?清关和销售模式是怎样的?"], ["昆明滇油石化有限责任公司是哪家企业?", "滇油石化在昆明吗?", "昆明滇油石化有限责任公司全称是什么?", "滇油石化这家公司名字怎么写?"], ["入驻园区的企业一共有多少家?", "园区入驻企业名单总共有39家对吗?", "李政德是谁?他在园区负责什么?", "李政德是云南国土资源职业学院哪个专业的?他什么时候毕业?", "地球物理勘探专业是哪个学校开的?李政德读的是本科还是专科?"], ["生物基纳米驱油剂是干什么用的?", "这家企业主要做低渗透油藏的什么产品?", "他们给中石油、中石化哪些油田提供服务?", "老油田二次采油和三次采油有什么区别?他们方案绿色高效体现在哪?", "一带一路沿线哪些国家的老油田会用他们的产品?"], ["Facebook越南市场卖普洱茶和建水紫陶壶这个项目具体是做什么的?", "有没有在Facebook上卖云南普洱茶和建水紫陶的跨境项目?", "越南市场做普洱茶电商是不是用Facebook平台?", "建水紫陶壶出口越南,是通过Facebook做的吗?", "这个跨境电商项目名称叫什么?"], ["罗有艳是哪个公司的负责人?", "罗有艳学的是越南语专业吗?哪年毕业的?", "云南农业大学越南语专业是本科吗?罗有艳是2020届的吗?", "罗有艳负责的项目跟越南有关吗?", "罗有艳毕业于云南农大哪个学院?"], ["园区里做普洱茶和建水紫陶跨境生意的是哪家公司?", "有没有专门做越南市场茶叶和茶具出口的企业?用的是Facebook直播带货那种模式吗?", "入驻企业里谁在搞‘内容种草+直播转化+9610报关+越南专线直邮’这一套跨境闭环?", "卖云南手工茶具、普洱茶,还主打越南市场的公司有几家?都在哪个园区?", "我想找做中式茶礼套装跨境零售的团队,他们是不是靠Facebook主页、短视频和广告引流?"], ["鬼才明AI创意工作室是干啥的?名字挺特别,是正式注册的项目吗?", "园区里有个叫‘鬼才明’的AI工作室,具体做什么方向?接不接企业定制开发?", "听说有个‘鬼才明AI创意工作室’,是学生创业项目还是老师带队的?", "‘鬼才明AI创意工作室’属于哪个企业或团队?有没有官网或联系方式?", "园区项目列表里的‘鬼才明AI创意工作室’,主要提供哪些AI相关服务?"], ["入驻企业里负责人叫赵广明、学计算机的那家是做什么的?", "赵广明是哪家企业负责人?重庆大学计算机系2022届毕业的,现在在园区做什么业务?", "园区里哪个公司的老板是重大的计算机毕业生,叫赵广明?", "赵广明负责的企业主营什么?他是不是刚毕业就创业了?", "查一下入驻企业名单,负责人姓名是赵广明、专业是计算机、毕业年份2022年的公司信息?"], ["2026年4月启动的那个产教融合AI项目,具体服务政校企哪些方面?", "有没有做AI数字人内容矩阵、还帮学校搞新商科专业规划的公司?", "园区里提供AI职业技能培训、赛事集训和资格认证的机构是哪家?", "哪个团队能帮政府或高校做AI场景应用落地、全域获客和品牌全案?", "聚焦AI商业化落地、数智化品牌赋能,还搞创新创业赛事运营的是哪家入驻企业?"], ["昆明云韵体育公司是不是管男子自行车洲际队的?", "园区里有支男子自行车洲际队,背后运营公司叫啥名字?", "‘昆明云韵体育有限责任公司’除了体育培训,还运营专业自行车队吗?", "想了解园区内职业自行车队的情况,归属哪家公司?是洲际级别吗?", "云韵体育这家公司是干啥的?听说他们带一支男子自行车洲际队?"], ["入驻企业名单里那个舒江贵是谁?他什么时候毕业的?学的什么专业?", "舒江贵是园区哪家企业的负责人?他是不是麒麟区职业学校毕业的?", "运动训练专业毕业的舒江贵,在园区负责哪家企业?", "园区入驻企业负责人舒江贵的学历背景是什么?", "舒江贵是哪年从麒麟区职业学校毕业的?学的是运动训练专业吗?"], ["男子自行车洲际队项目是干什么的?准备什么时候成立?", "云南要建的UCI男子自行车洲际队,具体怎么运营?", "这个自行车队项目是不是要在云南注册?目标是打亚洲比赛吗?", "听说有个高原训练+体育文旅+青苗培养的一体化自行车项目,是哪个?", "男子自行车洲际队项目靠不靠谱?它怎么把职业赛事、商业赞助和文旅结合在一起?"], ["‘男子自行车洲际队项目’在申报材料里被归到哪一类了?", "OPC是什么意思?自行车队项目为什么标着OPC?", "第5节申报名单里,自行车队项目属于OPC类,这个分类对入驻有影响吗?", "OPC类项目包括哪些?自行车队项目为啥被划进OPC?", "园区里还有哪些项目和自行车队一样属于OPC类别?"], ["附件1.3里的31家入驻实体名单完整版能给我看看吗?", "省级申报口径的入驻名单有31家,都有哪些公司或项目?", "‘米勒克尔蓝宝石’‘中越生物’这些项目在31家名单里排第几?", "园区入驻的31家单位里,有没有叫‘男子自行车洲际队项目’的?排多少号?", "‘仰光客厅中缅文化体验营地’‘PineSound派音智能’这些项目都在31家名单里吗?"], ["为什么同一个项目在不同表格里名字不一样?比如‘欧森纳东盟香港留学’和‘中越生物’改名了?", "入驻企业信息表和附件1.3名单名字对不上,是填错了还是有别的原因?", "两份名单项目名称有出入,比如‘米勒克尔蓝宝石’,是因为填报时间不同吗?", "园区不同表格里项目名称不一致,是口径不同还是数据没同步?", "‘中越生物’在两个表里名字不一样,是不是因为改名了?官方以哪个为准?"], ["昆明市大学生创业园的入驻管理规程是哪个部门发布的?", "这份《昆明市大学生创业园创业企业(项目)入驻管理规程》是哪年哪月哪日发布的?", "《昆明市大学生创业园创业企业(项目)入驻管理规程》的发布单位是哪个?", "这个管理规程的出处文件名是什么?", "2026年3月13日发布的园区入驻管理文件全称是什么?"], ["昆明市大学生创业园的主管部门是哪个单位?", "谁在管昆明市大学生创业园的入驻事务?", "负责统筹昆明市大学生创业园入驻工作的政府部门是哪家?", "昆明市人社局是不是创业园的主管单位?", "园区入驻管理归哪个局管?"], ["昆明市大学生创业园的入驻流程由谁具体操作?", "实际帮创业者办入驻手续的是什么机构?", "政府委托哪家单位来执行入驻全流程服务?", "入驻申请找谁办?是人社局直接办还是外包给别的公司?", "第三方服务机构在园区入驻中具体干些什么?"], ["昆明市人社局在入驻管理中只负责监督考核吗?", "主管部门除了监督,还参与入驻审核或审批吗?", "人社局对第三方服务机构怎么管?", "验收入驻工作是谁来做的?", "监督、考核、验收这三项职责都是人社局的吗?"], ["想进昆明市大学生创业园创业,要满足哪些硬性条件?", "申请入驻昆明市大学生创业园需要符合哪些要求?", "园区入驻门槛有哪些?", "大学生创业项目进园区得达到什么标准?", "入园资格审查主要看哪些方面?"], ["我是刚毕业两年的大学生,想创业开公司,能申请入驻园区吗?", "留学生回国创业,没在国内上过大学,可以入驻这个园区吗?", "我毕业已经7年了,还能算作符合条件的青年创业者吗?", "我们公司是外国企业,但想在昆明落地项目,能申请入驻吗?", "如果我不是法人,只是团队负责人,能代表项目申请入驻吗?"], ["园区重点支持哪些行业的创业项目?", "做绿色农产品电商的项目符合园区的产业方向吗?", "搞人工智能应用开发的公司属于园区鼓励的数字经济范畴吗?", "文化旅游和花卉相关的创业项目,园区会优先考虑吗?", "新能源汽车零部件研发算不算新材料或新能源产业链里的项目?"], ["入驻需要提交详细的商业计划书吗?", "启动资金有没有最低要求?没有实际出资能申请吗?", "公司之前有轻微的税务异常记录,还能申请入驻吗?", "团队有成员曾涉及知识产权官司,会影响整个项目入驻吗?", "园区对企业的内部管理制度有具体检查标准吗?比如必须有财务制度或人事规章?"], ["参加过‘春城创业荟’拿了二等奖,入驻时有加分或优先权吗?", "我们团队有国家发明专利,但没得过奖,算优先入驻对象吗?", "博士期间发了多篇顶刊论文,算‘突出学术成就’吗?", "获得省级创新创业大赛铜奖,符合优先入驻条件吗?", "手上有2项实用新型专利,能算重大发明创造吗?"], ["入驻园区一共要走几个步骤?大概多久能办完?", "从提交申请到正式入驻,中间要经过哪些环节?", "线上能全程办理入驻手续吗?还是必须现场跑几次?", "入驻流程里需要找园区哪个部门对接?有专门的联系人吗?", "材料初审通过后,后续还有评审或答辩环节吗?"], ["申请园区入驻需要准备哪些材料?", "入驻园区要交哪些表格和证件?", "第一次申请园区入驻,得带身份证、学历证这些吗?", "创业公司还没注册营业执照,能申请园区入驻吗?", "提交入驻申请时,创业计划书和资质专利证书是必须的吗?"], ["材料不全的话园区会怎么处理?", "初审没通过是不是就直接被拒了?", "服务机构初审时发现材料缺东西,会告诉我补什么吗?", "初审阶段如果漏交了某份文件,还能补交吗?", "园区初审是一次性指出所有问题,还是分几次通知?"], ["入驻评审是怎么进行的?", "路演答辩环节具体怎么安排?", "评审专家是从哪里请来的?是昆明本地的吗?", "入驻打分满分100,多少分才算过关?", "评审结果是当场出还是等几天?80分以上是不是就稳了?"], ["团队资质这块评分占多少分?", "评审时怎么看我们团队行不行?", "学历不高但有多年创业经验,团队分会不会很低?", "团队人员流动性大,会影响20分里的‘人员稳定’项吗?", "专业跟项目不完全对口,‘专业匹配度’这部分还能拿高分吗?"], ["项目创新性这部分占多少分?", "没有专利但有独特商业模式,算不算有创新性?", "技术壁垒指的是什么?我们用的是成熟技术但做了优化,算吗?", "自主知识产权必须是发明专利才行吗?软著或实用新型可以吗?", "行业壁垒高但技术本身不新,创新性这25分还能拿满吗?"], ["企业入驻评分里‘产业适配与市场前景’占多少分?这部分主要看哪些方面?", "匹配重点产业链、市场需求、竞品分析这些,是不是都算在25分的‘产业适配与市场前景’里?", "入驻打分中,市场拓展能力和盈利模式归在哪项?分值是多少?", "跟园区重点产业链贴合度不高,会影响入驻评分吗?这项占几分?", "‘产业适配与市场前景’25分具体怎么打分?需要提供竞品分析报告吗?"], ["运营规划和落地能力占多少分?商业计划书不完整会被扣分吗?", "启动资金和经营管理制度这些,属于评审里的哪一块?分值是多少?", "入驻打分中,有没有阶段目标和落地方案的要求?占多少分?", "光有想法没有详细执行计划,会影响入驻吗?对应的是哪项评分标准?", "‘运营规划与落地能力’15分,具体要提交哪些材料来证明?"], ["社会价值与发展潜力占多少分?带动就业和税收预期各占多大比重?", "入驻评审里,行业潜力和可持续发展归在哪一项?分值是多少?", "如果项目能带动几十人就业,但短期税收不高,社会价值这块能得高分吗?", "‘社会价值与发展潜力’15分,是不是主要看能招多少人、交多少税?", "园区看重长期发展,那行业潜力和可持续发展在评分里怎么体现?分值多少?"], ["入驻评审结果要在哪个网站公示?公示期多久?", "人社局官网公示后没人提意见,是不是就自动算通过了?", "评审完还要报哪个部门备案?备案之后才公示吗?", "公示环节是必须的吗?没公示能不能直接入驻?", "市人社局官网公示无异议后,下一步是什么流程?"], ["签《入驻孵化协议》是在公示之后还是之前?", "协议签完就能马上拿到工位吗?办公场地怎么分配?", "正式入驻的标志是签协议还是拿到工位?", "《入驻孵化协议》签了以后,多久能安排办公场地?", "签完协议、分到工位,是不是就代表可以正式入驻开始办公了?"], ["OPC创业空间适合哪些类型的创业项目?能待多久?", "大学生没注册公司,能在园区哪个区域孵化?最长能待几年?", "创业苗圃区和加速区有什么区别?没注册的团队能进加速区吗?", "国际创客区是给谁用的?配套服务跟其他区一样吗?", "归国留学生创业,能申请哪个孵化区域?入驻时间怎么算?"], ["孵化加速区对企业注册状态有要求吗?最多能待几年?", "初创企业申请加速区,需要提供营业执照吗?最长能续多久?", "已注册的初创公司,适合申请哪个孵化区域?能住满3年吗?", "加速区和国际创客区的入驻时长一样吗?国际创客区也要注册公司吗?", "国际创客区的‘个性化办公空间’具体指什么?跟加速区的独立办公空间一样吗?"], ["园区每年怎么考核入驻企业?考核内容包括哪些方面?", "入驻企业每年要参加什么考核?考核标准是怎么定的?", "园区对创业项目的年度评估都看哪些指标?有没有打分规则?", "企业年度考核会查营收、专利这些吗?还有别的考核点吗?", "园区怎么判断一家企业是不是适合继续留在孵化器里?"], ["经营发展能力在考核里占多少分?具体看哪些指标?", "技术创新能力考核包括研发投入和产学研合作吗?分值是多少?", "合规履约情况具体考什么?比如材料报送不及时会扣分吗?", "创业带动成效是不是要看招了多少应届生?实习岗位也算分吗?", "发展潜力这块主要看融资和资质认证?满分15分怎么打?"], ["年度考核多少分算合格?不合格会怎么样?", "考核80分以上就是优秀?有没有奖励或优先续签?", "如果考核只有59分,会被清退吗?有没有补救机会?", "考核结果分几个等级?每个等级对应什么后续安排?", "园区对考核不合格的企业会直接赶出去,还是先提醒整改?"], ["企业孵化不合格会被赶出去吗?", "孵化不合格会收到什么通知?", "整改后还是不合格,园区会怎么处理?", "整改通知书是什么意思?", "孵化协议会被解除吗?"], ["园区企业退出有哪几种情况?", "企业退出园区分哪些类型?", "退出管理包括哪些情形?", "企业从园区离开有哪些方式?", "园区对退出是怎么分类的?"], ["什么情况下算成功毕业出园?", "企业满足哪些条件就能出园了?", "出孵标准具体有哪些?", "怎么才算孵化成熟可以离开?", "自主核心产品规模化是不是出园条件之一?"], ["孵化期满了不续签会怎样?", "孵化到期没申请延期怎么办?", "延期申请没通过是不是就得走?", "已经延期过一次,到期了还能再延吗?", "孵化合同到期不续,园区会让我搬走吗?"], ["企业自己想退出园区要怎么办?", "因为经营困难能主动申请退出吗?", "团队散了还能申请退出吗?", "主动退园需要走什么流程?", "不想继续孵化了,怎么向园区提出退出?"], ["企业入驻后多久没人办公会被强制清退?", "哪些情况会导致园区勒令企业退出?比如空置、违规、转租这些算吗?", "企业被吊销营业执照或者破产了,园区会怎么处理?", "不配合检查、虚报数据或者擅自改项目,会被赶出去吗?", "连续几天办公室没人,或者一个月内累计多少天没人办公就算违规?"], ["园区平面图里那个带玻璃顶的地方是哪里?", "中庭、共享工位区和休息厅在图上分别是什么颜色、在什么位置?", "整个楼层布局是怎么安排的?比如大厅、过道、房间这些怎么分布的?", "顶部左侧的‘玻璃顶’和中上部的‘大厅’是不是同一个区域?", "最下面一排有9个房间,它们是连着一条过道的吗?"], ["成长区、国际区、加速区各有多少间办公室?", "共享工位区、大小会议室和玻璃顶这些算不算独立分区?数量怎么算?", "‘OPC’在布局图里对应哪个区?和宣传材料里的分区命名一致吗?", "休息厅、过道、玻璃顶这些功能空间有没有编号或具体数量?", "详细的布局图里提到的‘楼道’有多少条?分布在哪些位置?"], ["玻璃顶、成长区、国际区这些分区在楼层里是怎么上下左右排列的?", "共享工位区和休息厅是在楼层中间偏上的位置吗?", "加速区是排在最下面一排还是沿着过道分布的?", "大小会议室和加速区都在过道附近吗?它们和楼道的关系是什么?", "《入驻企业信息表》里的‘加速/国际/成长/OPC’分类,在实际布局里怎么一一对应的?"], ["昆明市创业园宣传册一共多少页?主要内容有哪些?", "宣传册里有没有介绍园区的空间布局或者分区类型?", "16页彩页的宣传册里,图片部分包含哪些关键信息?", "宣传册PDF文件里有没有提到企业入驻流程或管理规定?", "这份宣传册是官方发布的吗?主要面向哪些人群?"], ["园区的定位和核心理念是什么?", "这个园区主要想实现什么目标?", "园区倡导的理念有哪些?", "园区是怎么定义自己的发展方向的?", "园区的整体定位和愿景是什么?"], ["这个园区是免费的吗?", "大学生创业能在这里得到免费孵化服务吗?", "是不是政府办的创业园区?", "有没有专门针对青年大学生的创业孵化基地?", "这是不是云南第一家政府主办的大学生创业孵化园?"], ["园区是哪一年成立的?", "昆明市人社局主办的创业园什么时候开始运营的?", "这个园区在2012年获得了什么称号?", "园区具体地址在哪里?在几楼?", "园区总面积有多大?在昆明哪个位置?"], ["园区提供哪些创业支持服务?", "什么是‘七位一体’创业扶持体系?", "园区能帮创业者解决哪些问题?", "有没有AI相关的创业支持?", "园区能不能提供融资、政策、资源等一站式服务?"], ["园区面向南亚东南亚有什么特别规划吗?", "园区怎么体现国际化定位?", "AI技术在园区服务中怎么应用?", "来昆明创业的年轻人,这里是不是第一站?", "园区是不是专门为来昆明创业就业的青年打造的‘家园’?"], ["园区政策服务包括哪些主要板块?", "政策服务一共有几大类?", "宣传册里提到的政策服务有哪七个部分?", "园区的政策服务具体分哪几块?"], ["园区提供免费办公场地吗?", "新创企业能申请免费工位或独立办公室吗?", "办公桌椅、文件柜、高速网络这些基础设备园区包不包?", "有没有免费的基础办公设施可以使用?", "初创团队能拿到免费办公空间吗?"], ["园区有没有创业指导服务?", "有没有一对一的创业辅导?", "园区会帮创业者梳理商业模式和对接市场吗?", "创业沙龙、项目路演这些活动定期办吗?", "有没有专家团队帮我们做技术指导和创业规划?"], ["园区能帮忙找投资吗?", "有没有种子资金或者合作基金可以申请?", "园区能推荐银行贷款或股权融资渠道吗?", "创业初期怎么获得融资支持?", "园区跟创投机构有合作吗?"], ["园区能帮忙申请创业补贴吗?", "税收减免和社保补贴这些政策园区能协助办理吗?", "高校毕业生创业有哪些扶持政策,园区能帮着落实吗?", "创业扶持资金怎么申请?园区会协助吗?", "政策对接服务具体都包括哪些内容?"], ["园区能帮我们对接高校和科研院所吗?", "如果想跟昆明的大学或大公司合作技术或者招人,园区能帮忙吗?", "园区是不是属于昆明创业创新服务体系?能参加‘春城创业荟’这类活动吗?", "我们项目想做成果转化,园区能提供对接支持吗?", "园区有没有资源帮初创企业找技术合作伙伴或者引进人才?"], ["园区有没有免费的AI培训?都教些什么内容?", "想学怎么用AI做内容创作或者开一人公司,园区有相关培训吗?", "听说园区有AI公益课,能教企业怎么用AI省钱增效吗?", "园区的AI培训是实操性质的吗?有没有老师手把手教?", "除了AI应用,园区还开哪些创业相关的免费培训课?"], ["注册公司的事园区能代办吗?", "刚创业,税务和知识产权这些专业事园区能帮忙处理吗?", "园区提供法律援助服务吗?比如合同纠纷或者劳动问题?", "工商注册、报税这些流程复杂,园区有专人协助办理吗?", "知识产权咨询是免费的吗?包括商标和专利方面的指导?"], ["在校大学生能申请入驻园区吗?", "毕业6年的本科生还能以创业者身份申请入园吗?", "外籍留学生创业,符合园区的入驻条件吗?", "园区重点支持哪些行业的创业项目?比如做生物医药或者新能源的行不行?", "我们项目是做数字文旅的,属于昆明‘10+N’产业链里吗?"], ["入驻需要提交详细的商业计划书吗?", "启动资金要多少才够?园区对创业者的资金能力有硬性要求吗?", "公司之前没出过问题,但没申请过专利,会影响入驻吗?", "如果团队有发明专利或者拿过市级创业比赛奖,是不是更容易入园?", "园区查不查企业的信用记录?比如有没有违法或经营异常?"], ["入驻园区要走哪些步骤?整个流程是怎样的?", "从咨询到正式入驻,一共要经过哪几个环节?", "路演答辩后多少分才算通过?评审结果在哪公示?", "入驻评审要答辩吗?有没有打分标准?多少分能过?", "市人社局官微会公示哪些信息?公示期多久?"], ["OPC创业空间适合做AI应用的团队吗?能直接拎包入住吗?", "我们是还没注册公司的大学生团队,能申请哪个区?共享工位能用多久?", "创业苗圃区待满两年后,表现好还能继续留园吗?怎么转区?", "孵化加速区给初创企业配的是独立办公室吗?最长能待几年?", "留学生回国创业,该申请哪个区域?配套和普通区一样吗?"], ["国际创客区和孵化加速区的入驻时长规定是一样的吗?", "归国留学生申请国际创客区,办公空间是定制的吗?有哪些国际化配套?", "孵化加速区的企业续期需要什么条件?最多能待三年对吗?", "初创企业已经注册了,但刚成立一个月,符合孵化加速区要求吗?", "创业苗圃区和孵化加速区在工位类型上有什么区别?"], ["咨询电话是多少?打哪个号问入驻报名最靠谱?", "18087174891这个号码是打给谁的?报统计表也打这个吗?", "云南省就业局那边怎么申报?和园区直报流程一样吗?", "报名咨询除了打电话,还有别的联系方式吗?比如微信或网站?", "填报人电话和宣传册上的咨询电话重复了,是不是同一个联系人?"], ["园区办公地址在哪?具体到门牌号和楼层?", "昆明市大学生创业园是在昆明市人力资源中心里面吗?", "去现场咨询要上几楼?是在民航路229号的二楼吗?", "人力资源中心二楼除了创业园,还有其他办事窗口吗?需要预约吗?", "导航搜‘昆明市大学生创业园’能准确定位到民航路229号吗?"], ["报名材料发到哪个邮箱?", "报名要发邮件到什么地址?", "接收报名的邮箱是多少?", "报名是通过邮箱提交吗?", "714066050@qq.com 是报名用的邮箱吗?"], ["园区都跟哪些媒体合作做宣传?", "园区怎么对外宣传创业典型?", "电视、广播、报纸这些渠道园区都用吗?", "地铁专列和网络直播也是园区的宣传方式吗?", "园区的媒体报道覆盖哪些平台类型?"], ["园区合作的全国性媒体有哪些?", "央视网、人民网这些算不算园区的合作媒体?", "抖音和今日头条是不是园区的全国合作媒体?", "网易搜狐这些大平台跟园区有宣传合作吗?", "CCTV.com和people.cn是园区对接的主流媒体吗?"], ["云南本地的媒体哪些跟园区有合作?", "云南日报和云南台是不是园区合作的省级媒体?", "春城晚报和云南网也在园区的合作名单里吗?", "Yunnan Media Group指的是云南广播电视台吗?", "园区在云南省内主要跟哪些媒体联动?"], ["昆明市有哪些媒体和园区合作?", "昆明日报和昆明广播电视台是不是园区的市级合作媒体?", "掌上春城和无线昆明是园区合作的本地平台吗?", "KMCC是不是指掌上春城?它跟园区有宣传合作吗?", "园区在昆明市主要通过哪些媒体做报道?"], ["园区的新闻热线是多少?", "想联系园区发新闻稿,电话是多少?", "8099999这个号码是园区的媒体热线吗?", "打哪个电话可以对接园区的媒体合作?", "园区对外的新闻联络方式有哪些?"], ["园区宣传册上用的数据口径是哪些?", "对外宣传时园区推荐用哪些官方数据?", "新闻报道里提到的园区数据,有没有统一的标准说法?", "园区有没有指定的对外宣传数据模板或参考口径?", "做宣传材料时,哪些园区数据是官方认可、可以引用的?"], ["昆明市政府网站上怎么介绍园区的规模和成效?", "园区运营多久了?有多少企业入驻?带动多少创业者?", "昆明市政府官网说园区入驻企业超1000家,这数据是截至什么时候的?", "园区运营超过10年,带动创业者超1万人次,这个说法出自哪里?", "昆明市政府网站提到的园区数据(比如1000+企业、1万+创业者)具体是哪篇报道的口径?"], ["春城晚报报道过园区孵化企业的成绩吗?", "园区近3年孵化了多少家企业?培育了多少高新和科小企业?", "春城晚报说园区孵化超100家企业,其中5家是国家级高新,12家是省级科小,这准确吗?", "有没有媒体报道过园区在培育高新技术企业方面的成果?", "春城晚报提到的园区企业培育数据(如5家国高、12家省科小)具体指什么?"], ["昆明日报最近报道过园区新增企业情况吗?", "2024年以来园区新入驻了多少家企业?其中高新企业有多少?", "园区今年办了多少场活动?覆盖多少青年创业者?", "昆明日报说园区2024年新增32家企业、举办126场活动,这些数据能确认吗?", "昆明日报提到的‘2024年以来新增高新技术企业15家’,是指新认定还是新入驻?"], ["媒体报道里说的孵化企业数量跟政府报表上的数字不一样,这是怎么回事?", "对外宣传时提到的孵化企业数和官方统计的累计数据有出入,该怎么正确引用?", "为什么媒体报道口径和省级申报、月报表里的累计数据不一致?", "园区对外发布的孵化企业数、出园企业数、带动就业人数这些数据,是不是和官方统计有差别?", "媒体报道中用的那些数据,比如孵化181家,跟人社局正式报表里的数字对不上,哪个更权威?"], ["昆明市大学生创业园是哪年成立的?", "园区什么时候被评为省级创业园的?", "2024年4月园区焕新亮相是怎么回事?都有哪些媒体报了?", "央视网5G、中国网这些大媒体啥时候报道了园区焕新?", "园区2024年4月15日发布的第二期入驻招募公告,跟第一期评审结果公示是一起发的吗?"], ["2024年5月园区办了哪些重要活动?", "春城晚报和云南日报啥时候整版报道了园区?标题叫啥?", "‘聚力赋能·共创未来’这个高校创业活动是谁主办的?有哪些学校参与?", "昆明城市学院师生啥时候来园区参观交流的?", "青年企业家协会啥时候走进园区揭牌创新创业基地的?"], ["《入驻管理规程》是哪年哪月印发的?谁发的?", "2026年第一期入驻申请截止后,初审通过了多少个?复审又收了多少个?", "第一期集中评审具体怎么评的?流程是啥?", "2026年第一期最终有多少家企业或项目正式入驻了?", "2026年第二期入驻招募公告是啥时候发布的?"], ["‘创赢未来’2026创业大赛昆明选拔赛是啥时候结束的?谁主办的?", "马兰花创业培训讲师大赛跟园区有关系吗?是不是在园区办的?", "2026年创业大赛昆明选拔赛和讲师大赛是不是同一个活动?", "昆明市人社局主办的这场创业大赛,跟园区日常运营有啥关联?", "‘创赢未来’创业大赛昆明选拔赛落幕那天,园区有没有参与或者协办?"], ["昆明市公共就业和人才服务中心主办的创业路演活动什么时候开始?一共几天?有多少项目进了省赛?", "‘滇水逐梦·雨林创航’2026昆明·西双版纳创业路演PK赛是哪几家单位联合办的?园区里哪家企业参与了活动策划?", "园区最近有没有对入驻企业做实地摸排?摸排了多少家?发现什么问题?怎么处理的?", "云南师范大学和园区搞的就业创业实践基地揭牌仪式,具体有哪些国家的留学生参加了?主题是不是叫‘校地产教融合、跨境双创人才共育’?", "云南旅游职业学院和云南师范大学这两所学校,最近分别什么时候来园区开展就业创业交流活动的?"], ["园区的官方名称全称是什么?在哪些正式文件或设计稿里有明确出处?", "昆明市大学生创业园的定稿文件除了.ai源文件,还提供了什么格式的版本?比如PNG或者JPG?", "园区的官方文件定稿是叫《昆明市大学生创业园 定稿》吗?这个标题出现在哪里?", "园区有没有统一的品牌视觉识别规范?比如Logo、字体、配色这些?", "‘昆明市大学生创业园’这个名称在品牌VI里是怎么写的?英文全称是什么?"], ["园区Logo是什么样子的?颜色搭配是哪两种?字母K和M是怎么组合的?", "园区的主Logo是橙色和蓝色搭配吗?那个圆环形/动感造型是不是由K和M组成的?", "园区Logo里的英文全称怎么拼?是不是‘KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK’?", "园区VI里最常用的排版方式是怎样的?比如Logo和文字一般怎么摆放?", "园区品牌标识强调橙蓝配色,这个‘橙’和‘蓝’具体指哪种橙、哪种蓝?有没有标准色值?"], ["园区品牌VI规定的主色有哪些?橙色和蓝色的具体色值是多少?", "VI规范里说的品牌主色,是不是用设计软件拾色器取的数值?能直接用在PS或AI里吗?", "园区橙色和蓝色的标准色号分别是多少?CMYK、RGB或者HEX值有公布吗?", "除了橙蓝双色,园区VI里还有没有其他辅助色或禁用色规定?", "品牌主色的取值依据是设计稿里的拾色器读数,那这个‘设计稿’指的是哪个版本的文件?"], ["园区Logo的橙蓝双色组合,和VI里定义的品牌主色是不是完全一致?有没有细微差别?", "‘橙色K’和‘深蓝M’交织的Logo造型,在不同尺寸(比如名片vs展板)下会不会有简化版?", "园区对外宣传材料中,Logo必须居中排版吗?有没有允许左对齐或其他变体?", "英文全称‘KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK’在VI里有没有大小写或缩写规范?", "VI规范第14条讲的是品牌设计,那前面13条都包括哪些内容?比如字体、图标、应用延展这些?"], ["园区的主色调橙色和辅助色深蓝色的十六进制代码分别是多少?", "活力橙和深邃蓝对应的RGB数值各是多少?", "品牌VI里用的主色和辅色具体叫什么名字?分别用在哪些地方?", "橙色#FC8318和蓝色#022B97哪个是主色哪个是辅色?它们各自代表什么含义?", "园区LOGO和正文文字分别用的是哪种颜色?主色橙和辅色蓝的具体用途有哪些?"], ["昆明市大学生创业园的品牌VI源文件叫什么名字?在哪里能拿到?", "‘昆明市大学生创业园 定稿.ai’这个AI文件是做什么用的?", "主画板成品图和色彩规范参考图的文件名分别是什么?格式是png还是jpg?", "VI设计规范里提到的三个配套文件具体指哪几个?各自用途是什么?", "想改LOGO或者做宣传物料,该找哪个源文件?有没有成品图和配色参考图?"], ["绩效数据里的‘认定以来累计’‘本年度’‘当前实有’分别是什么意思?", "为什么本月报表说累计出园149户,省级申报却写150户?差这1户是怎么回事?", "不同表格中数据口径不一致时,应该以哪个为准?", "‘填报时点不同’具体指什么?会影响数据准确性吗?", "数据口径说明里提到的‘照原文保留’是什么意思?是不是不能自行修改差异?"], ["绩效表里‘累计数’和‘当年数’怎么区分?分别对应哪些章节?", "第2节绩效和第3节月报表的数据分类方式一样吗?", "‘认定以来累计’‘本年度’‘当前实有’这三个统计维度在报表里怎么体现?", "园区绩效数据的统计逻辑是按自然年、财政年还是认定周期?", "为什么同一指标(比如出园户数)在不同表格里数值不一样?背后的数据逻辑是什么?"], ["园区现在一共入驻了多少个项目?内部统计和省级申报为什么数量不一样?", "信息表列了39家,省级申报只列31家,少了8家是没通过认定吗?", "内部全量项目名单和省级认定名单的项目名称为啥有的不一样?是改名了吗?", "39家和31家这两个数字分别代表什么范围?是否包含孵化中和已出园的项目?", "项目名称在不同名单里有出入,是命名规范不统一,还是认定标准导致的差异?"], ["宣传册里写的媒体报道数据和官方申报的累计数据不一样,该怎么引用才规范?", "外宣口径和官方申报口径的数据对不上,引用的时候要注意什么?", "宣传册里的媒体报道数据跟别的地方不一样,引用时要怎么注明来源?", "第12节提示里说的引用出处具体指什么?"], ["文件里的数值是直接抄的原始资料,还是自己算出来的?", "这些数字是原始数据还是加工过的?", "文档里的数值有没有经过推算或调整?", "所有数据都是直接从原始文件拿的,没改过吧?"], ["这份文档是哪年生成的?", "文档生成日期写的是2026年,是未来时间吗?是不是填错了?", "整理这份文档用的原始文件格式有哪些?", "文档说是根据Excel、PDF这些原始资料整理的,具体支持哪些格式?"]]} \ No newline at end of file diff --git a/backend/knowledge/kb_vectors.npy b/backend/knowledge/kb_vectors.npy new file mode 100644 index 0000000..a11715d Binary files /dev/null and b/backend/knowledge/kb_vectors.npy differ diff --git a/backend/knowledge/opc.md b/backend/knowledge/opc.md new file mode 100644 index 0000000..81b37e4 --- /dev/null +++ b/backend/knowledge/opc.md @@ -0,0 +1,227 @@ +# OPC(一人公司)知识库 + +> 口径说明:以下政策与数据基于公开资料整理,截至 2026 年 8 月。涉及具体补贴金额、名额、申报条件时,以云南省及昆明市最新官方文件为准;本库作为语义检索与回答口径的参考。 + +## 一、什么是 OPC(一人公司) + +- 定义:一人公司(One Person Company, OPC)指只有一个股东的公司,分为一人有限责任公司和一人股份有限公司两类。 +- 法律沿革:传统公司法理论认为公司属社团法人、应有 2 人以上股东;我国自 2005 年修订《公司法》起正式引入一人有限责任公司。 +- 时代定位:在 AI 时代,一人公司成为低成本、高效率、普惠型的创业形态——创业者仅凭一人之力,借助 AI 工具即可构建覆盖产品研发、市场营销、客户服务与财务管理的完整业务闭环。 +- 核心理念:「你负责商业判断,AI 负责高效执行」,实现从"出卖单次时间"到"经营自动化系统"的跨越。 +- 与自由职业的区别:一人公司具备单一股东、有限责任、极简架构等正规企业属性,同时兼具灵活高效、税负更可控等优势。 +- 规模:截至 2025 年 6 月,全国一人有限责任公司已超过 1600 万家。 + +## 二、新《公司法》对一人公司的调整(2024 年 7 月 1 日施行) + +- 总特征:准入放宽与责任强化并存。 + +(准入放宽) + +- 允许设立一人股份有限公司:设立股份有限公司应有一人以上二百人以下发起人,一名股东也可设立股份有限公司。 +- 取消一人股东数量限制:删除旧法"一个自然人只能投资设立一个一人有限责任公司,且该一人公司不能投资设立新的一人公司"的规定;一个自然人可设多个一人公司,可形成投资链条。 +- 放宽股东类型:删除"一人公司股东只能是自然人或者法人"的限制,个体工商户、合伙企业、个人独资企业等理论上也可作股东设立一人公司。 +- 删除专节规定:不再采用"一人有限责任公司"或"一人股份有限公司"的特殊表述,统一为"只有一个股东的公司",纳入普通公司规制。 +- 删除年度审计强制要求:不再强制一人公司每年编制财务会计报告并审计。 +- 删除登记特别注明:不再要求在登记及营业执照中注明"自然人独资"或"法人独资"。 + +(责任强化,需重点注意) + +- 举证责任倒置:新《公司法》第 23 条第 3 款规定,只有一个股东的公司,股东不能证明公司财产独立于股东自己财产的,应对公司债务承担连带责任。 +- 财产混同是主要认定依据:司法实践中,公司账户与股东个人账户资金往来频繁是财产混同的主要认定依据。 +- AI 时代"数字资产混同"风险:用个人名义注册付费订阅 AI 大模型、购买云端算力、将训练数据与生成物存于个人账号,一旦发生纠纷,可能被认定公司缺乏独立财产基础,判令股东承担连带责任。 +- 不设股东会时须书面决定:股东就利润分配、亏损弥补、修改章程等事项,须以书面形式决定,签名或盖章后置备于公司。 + +(合规建议) + +- 建立健全财务管理制度,账目清晰、独立核算,严格区分公司与个人财产界限。 +- 适时出具连续完整的会计年度审计报告,或股东与公司往来专项审计报告,作为财产独立性的有力证据。 +- 将合规视野从"财务隔离"拓展到"数据与数字资产隔离"。 + +## 三、OPC 创业思路与经验 + +(方法论核心) + +- 先完成,再完美:AI 已把门槛拆掉,不要等学会所有技能再开始。 +- 最小成本验证模式:用 AI 思考与决策,用外包解决人力,用最小成本验证商业假设。 +- 聚焦四力:产品能力(解决什么问题)、流量能力(客户如何找到你)、交付能力(如何高效完成服务)、变现能力(如何设计商业模式)。 + +(盈利三把钥匙与商业模式) + +- 盈利三把钥匙:用 AI 降本增效、用信任创造溢价、用平台借势腾飞。 +- 六大主流商业模式:知识付费(课程/咨询/私教)、内容变现(公众号/小红书/抖音)、服务外包(代运营/代写/代设计)、工具产品(模板/插件/SaaS)、社群运营(付费社群/会员制)、混合模式。 + +(创业路径建议) + +- 从盘点个人技能 → 验证市场需求 → 找准差异化定位 → 用极低成本测试 MVP → 搭建自动化内容引流矩阵与私域池 → 打造标准化交付质控系统 → 设计高溢价产品矩阵 → 构建抗风险的多元化收入线。 + +(案例经验) + +- 电商:河南创业者卧床 70 天,用 AI 替代原计划的 10 人团队做跨境电商,开出 12 家网店(后优化至 4 家主力店),单店月销 5-8 万、月净利 2.5-3 万,每天只需工作 2-3 小时;用扣子搭工作流自动生成经营日报。 +- 内容与服务:无锡创业者 2025 年转型"企业陪跑",帮企业做小红书内容转化;指出 OPC 两大难题是"如何定价"与"如何建立交易机制(个人即产品、前期缺乏标准化交付物)"。 +- 全 AI 员工公司:创业者用开源 AI 代理框架创建"零人类公司",业务含卖教程、AI 技能市场抽佣、企业定制代理,几周收入近 20 万美元,运营成本仅约每月 1500 美元。 +- 大厂裸辞:35 岁管理者裸辞环游多国,靠 AI 创立一人公司,兼任大模型工具开发者、企业咨询顾问、自媒体博主,年入近百万。 + +(清醒认知与挑战) + +- AI 没有消灭赚钱的门槛:认知、决策能力甚至运气同样重要;没有商业逻辑,再好的工具也没用。 +- 理性看待收入神话:网上"一人公司月入十万"的故事大多不靠谱,成功相当程度依赖运气。 +- 一人公司的孤独感与亲力亲为:现金流、回款、定价等都需要自己承担。 + +## 四、云南当前生效的 OPC 超级个体政策 + +(省级专项:2026 年 7 月云南省人社厅《关于支持服务人工智能 OPC 创业的若干措施》) + +- 创业生态社区:鼓励盘活闲置楼宇、高校实训场地等,整合算力、绿电、数据、市场渠道等要素,打造"低成本空间+普惠算力+全链条生态服务"的 AI OPC 创业生态社区;运营效果好的可按创业孵化载体补助政策认定补助。 +- 免费工位与安居支持:支持现有创业园区开设 OPC 专区,提供拎包入驻免费工位;有条件地区为 OPC 创业人才提供免租或低租人才公寓、青年驿站。 +- 金融支持:"云岭创业贷"——企业法人最高可申请 500 万元信用贷款、3000 万元抵押担保贷款;个人创业者最高可申请 50 万元信用贷款、1000 万元抵押担保贷款;鼓励推出"算力贷""数智创新专项信贷"等专属产品。 +- 创业补贴:符合条件者给予 5000 元一次性创业补贴;电商创业项目每个最高补贴 3 万元。 +- 见习补贴:吸纳高校毕业生就业见习的按每人每月 1500 元(留用率达 50% 以上可增至 2000 元)发放见习补贴。 +- 培训补贴:AI 相关职业(如数据标注员)纳入培训补贴范围,最高可获 1700 元创业培训补贴和 7280 元技能培训补贴。 +- 人才政策:符合条件的 AI OPC 创始人纳入"兴滇英才支持计划"等省级人才计划;设立人工智能专业职称评审;打破学历、年龄、资历限制建立人才自主评价机制。 +- 政务服务:深化"个人创业一件事"集成办理,实现 OPC 企业登记一表申请、一网联办、一次办结。 + +(市级:昆明市大学生创业园) + +- 面向对象:在校大学生、毕业不超过 8 年的高校毕业生、归国及外籍留学生等青年创业者。 +- 入驻方向:项目须聚焦昆明市"10+N"产业链(人工智能、数字经济、电子信息、新媒体直播、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等);获市级以上创业大赛奖项、有突出学术成就或高新专利者优先。 +- 空间保障:免费提供办公工位或独立区域。 +- 孵化服务:专家一对一创业指导、商业模式梳理、沙龙路演。 +- 融资支持:联动创投机构设立种子资金,每年组织不少于 15 场融资对接活动。 +- 政策对接:协助申请创业扶持资金、税收减免、社保补贴。 +- 资源整合:对接高校、科研院所、龙头企业。 +- 专项赋能:免费提供"AI 育笋计划"和"AI+OPC"全套培训(涵盖 AI 应用、一人公司落地、成本控制等)。 +- 综合服务:工商注册、税务代办、知识产权咨询、法律援助一站式支持。 + +## 五、OPC 的几种创业模式 + +(OPC 核心范式) + +- OPC 的核心是「1 个创始人 + N 个 AI 数字员工」的 1+N 模式:创始人负责战略决策、创意设计与关键价值判断,AI 承担代码生成、内容创作、数据运营、客户服务等标准化重复工作。 +- OPC 将创业从「资本密集型」转向「认知密集型」:无需融资和组建团队,只需对领域有深度认知,再用 AI 将其转化为产品、服务或收入。 +- 创业门槛从「资源门槛」降为「认知门槛」:代码可用 Claude Code 写、内容可用 AI 生成、跨语言可实时翻译、客户对接可用 AI Agent 筛选。 + +(六大已跑通的 OPC 商业模式) + +- 专业服务型:以深度专业技能为核心价值,交付定制化解决方案或咨询,按项目或小时收费,利润率高。 +- 高端服务型:以行业认知与战略判断力为核心,提供高端顾问、私教、战略规划,高客单(一单可达几十万)。 +- 知识产品型:将系统化专业知识做成标准化数字产品(课程、电子书、会员),边际交付成本趋近于零,最被看好。 +- 数字产品型:可复用的软件或数字工具(SaaS、应用、插件、模板),订阅制或一次性/API 收费,规模化潜力最大。 +- 创意内容型:靠个人影响力或 IP,产出博客、视频、社媒内容,通过广告分成、品牌合作、知识付费变现。 +- 平台整合型:做资源连接与撮合,自身不生产产品,靠佣金或撮合费。 + +(如何选择适合你的模式) + +- 先想清三个问题:卖「能力」还是卖「系统」?服务边界清不清楚?你是「执行者」还是「系统设计师」? +- 顾问/咨询型适合专业壁垒深厚、客户关系强的人;AI 可解放线索收集、标准问答、方案初稿。 +- 内容/知识型放大个人 IP,内容可标准化复用、多平台分发,边际成本趋零,适合有表达与知识结构化能力的人。 +- 产品化服务型(SaaS/数字产品)将方法论打包成模板或工具,从「卖服务」转向「卖产品」,长期复利最可观但技术含量最高。 +- 热门赛道:健康/医疗/心理支持、硬件/IoT/机器人、教育/培训/知识服务、金融/法律/政务等专业服务、企业服务/SaaS。 + +## 六、如何成立一家 OPC(一人公司) + +(主体选择) + +- 先副业后主体:不建议立即辞职,先以副业验证模式。 +- 个体工商户注册快、税负低,适合早期验证、接小单;有限责任公司信用强、利于 B 端,适合长期发展,但务必公私分明避免财产混同。 +- 若业务面向 B 端长期合作选有限公司,早期验证可选个体户。 + +(注册流程) + +- 公司名称核准:提交 3-5 个备选名称(行政区划+字号+行业+有限公司),经市场监管局官网或「一网通办/一窗通」平台核准。 +- 提交设立登记申请:线上提交公司章程、股东身份证明、注册地址证明等,约 3-5 个工作日领取营业执照。 +- 刻制印章:公章、财务章、法人章,到公安备案刻章点办理,费用约 500 元。 +- 银行开户:预约开基本户,需带执照、公章、法人身份证,年费约 500-1000 元。 +- 税务登记:领取执照后 30 日内办理,核定税种、申请发票。 +- 新《公司法》允许单人担任法人且无需监事。 + +(注册资本与地址) + +- 实行认缴制,注册时无需实缴全部资金;但须在章程明确认缴期限,且 5 年内实缴到位。 +- 认缴≠不缴:切忌盲目追高注册资本,经营不善时股东需在认缴范围内承担责任。 +- 注册地址须为商用地址(写字楼、商铺、共享办公工位、集中登记地/虚拟地址等),住宅原则上不可用。 +- 可租用园区或共享工位,或用集中登记地/虚拟地址(部分园区免费或年费约 3000-6000 元)。 + +(记账报税与合规) + +- 公司必须建立账簿,即使零收入也须按月或按季申报(零申报),逾期可能被列入经营异常名录。 +- 小规模纳税人季度开票≤30 万免增值税(普票);小微企业年应纳税所得额≤300 万按 5% 缴企业所得税(2027 年底前)。 +- 一人公司利润分配后转账需缴 20% 分红个税。 +- 代理记账费用约 200-500 元/月,起步阶段建议找代账公司。 +- 每年 6 月 30 日前登录「国家企业信用信息公示系统」提交年报。 +- 严格区分公司财产与股东个人财产,避免公私账户混同,否则股东可能对公司债务承担连带责任。 + +## 七、如何选用合适的 AI 工具 + +(选型原则) + +- 按「场景适配」选工具,不要盲目追新:没有哪一款模型在所有任务上全面领先。 +- OPC 中 AI 首先承担软件开发角色(多数团队用 AI 编程),其次才是内容创作。 +- 优先选生态开放、能接 API/插件、支持工作流编排的平台,方便搭建自动化。 + +(按场景选模型) + +- 写代码、推理、性价比选 DeepSeek(开源、API 定价低)或智谱 GLM(编程强、可私有化)。 +- 中文通用与云生态选通义千问(阿里云百炼 MaaS、长上下文)。 +- 多模态、免费 C 端选豆包(字节),创意内容可用其视频/图像生成。 +- 长文档、研报处理选 Kimi(超长上下文)。 +- 政企、资质、央国企场景选文心一言(百度,合规资质全)或讯飞星火(语音、信创)。 +- 微信生态、快慢思考选腾讯混元。 + +(Agent 与工作流搭建平台) + +- 扣子(Coze):字节推出的零代码 Agent 平台,拖拽式搭建,适合个人/非技术用户,支持豆包、DeepSeek、Kimi、通义千问、GLM 等模型。 +- Dify:企业级、技术向,工作流节点丰富,支持国内外主流模型与私有化部署。 +- 用无代码平台可把客服、内容、竞品、线索、数据等搭建为智能体,月成本可低至约 170 元。 + +## 八、AI Agent(智能体)概念与一人公司应用 + +(概念) + +- AI 智能体是一套能自主理解目标、制定计划、执行操作、自我修正的完整闭环系统,不同于普通聊天机器人。 +- 核心公式:AI Agent = 大模型 + 记忆 + 主动规划 + 工具使用。 +- 三大特征:自主决策(自己判断下一步)、工具调用(操作浏览器、调用 API、读写文件、发邮件)、持续学习(优化策略)。 +- 成熟智能体通常含五个核心模块:大语言模型(大脑)、任务规划、记忆存储、工具调用、环境感知。 + +(如何搭建一个智能体) + +- 角色设定:赋予具体人员设置与知识边界,上传历史文档。 +- 知识库挂载(RAG):整理笔记、业务 SOP、白皮书,通过文档切片与向量检索让 AI 具备「记忆」。 +- 工作流设计:将重复劳动逻辑化,如自媒体运营分身:聚焦热点、生成文案、生成图表、多平台分发。 +- 插件与 API 集成:接入搜索、数据库、API 插件,让智能体具备「手脚」。 +- 避坑:防止幻觉(强制优先从知识库取答案)、遵循 MVP 原则不过度设计、定期分析对话日志迭代提示词。 + +(OPC 的 1+N 组织架构) + +- 核心层(1 人)只做四件事:定战略、定标准、审关键输出、管现金流。 +- AI 智能体层(N 个数字员工):按岗位拆分为独立 Agent,如产品/研发、内容/营销、流量/投放、销售/客服、交付/项目、财务/行政。 +- 外部协作层:把设计、剪辑、翻译等 AI 搞不定的重活外包。 + +(流水线工作流与运营闭环) + +- 标准工作流:需求输入、AI 拆解、AI 生成初稿、创始人审核微调、AI 优化排版、成品入库或上线。 +- 运营闭环:AI 批量产内容并多平台分发、AI 客服 7×24 响应并自动生成报价单、标准化 SOP 加 AI 自动交付、AI 记账报税。 +- 效果:启动成本可从传统创业的 20 万-100 万压缩到每月 500-5000 元;试错周期从半年缩短到 2-6 周。 + +## 九、国内主要大模型厂商与选型 + +(市场格局) + +- 国内大模型市场呈「三超多强」格局:第一梯队为豆包(字节)、通义千问(阿里)、DeepSeek;第二梯队为智谱 AI、文心一言(百度)、混元(腾讯);第三梯队为 Kimi、MiniMax 等创新企业。 +- 在简单问答、常识领域各大模型差异甚小,差异主要体现在复杂指令、多模态理解生成与多轮对话。 + +(各厂商定位) + +- DeepSeek:推理、代码、数学、性价比强,开源(MIT),API 定价低,适合写代码与高性价比推理。 +- 通义千问(阿里):通用、中文、企业云生态强,阿里云百炼 MaaS,长上下文,回答完整度突出。 +- 豆包(字节):C 端应用、多模态、Agent,日活高、免费多模态,深度逻辑与代码能力相对弱。 +- 智谱 GLM:编程、长程 Agent 任务强,开源、长上下文,政企私有化根基扎实。 +- 文心一言(百度):全模态、搜索、中文写作,功能完善度领先、合规资质最全。 +- Kimi(月之暗面):超长文档、研报解析领先。 +- 讯飞星火(科大讯飞):中文、语音交互、教育医疗,央国企与信创环境应用广。 +- 混元(腾讯):社交、多模态、快慢思考,微信生态深度整合。 + +(选型建议) + +- 写代码商用:智谱 GLM 或 DeepSeek;长文档处理:Kimi;多模态/免费 C 端:豆包;企业政企落地:智谱、文心一言或通义千问。 +- 各家价格口径不统一(有美元/人民币计、缓存命中与上下文位影响单价),比价务必回官网核对同一口径。 +- 首期成效:受理 58 个项目,23 个成功入驻,覆盖人工智能、低空经济、跨境服务等领域。 \ No newline at end of file diff --git a/backend/knowledge/park.md b/backend/knowledge/park.md new file mode 100644 index 0000000..1b525dc --- /dev/null +++ b/backend/knowledge/park.md @@ -0,0 +1,90 @@ +# 昆明市大学生创业园(OPC 智能园区)知识库 + +> 数据口径声明:本知识库内容全部依据《昆明市创业园宣传册》《昆明市大学生创业园入驻企业信息表》《昆明市大学生创业园运行情况统计表(2026年7月)》《附件1.省级创业孵化载体相关情况简介材料》《昆明市大学生创业园创业企业(项目)入驻管理规程》等标准资料整理;涉及运营数据时严格区分「官方统计口径」「省级绩效累计口径」「AI 平台运行指标」三类口径,不得混用。 + +## 一、园区基本情况(依据《宣传册》《省级创业孵化载体简介材料》) +- 全称:昆明市大学生创业园(KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK) +- 性质:云南省首家由政府主办、免费为创业青年大学生提供创业孵化服务的开放式园区 +- 成立时间:2009 年(2009 年 9 月成立) +- 主管部门:昆明市人力资源和社会保障局 +- 园区地址:昆明市民航路 229 号(昆明市人力资源中心 1—2 楼) +- 建筑面积:约 3000 平方米,可容纳约 50 个大学生创业团队入驻 +- 主导产业:数字经济与电子信息产业 +- 认定省级创业园时间:2012 年 +- 咨询电话:18087174891 / 17787129131 / 18214573650;接收邮箱:714066050@qq.com +- 服务模式:以「空间 + 孵化 + 融资 + 政策 + 资源 + AI 赋能 + 综合服务」七位一体全链条创业扶持体系,打造「一站式」创业孵化服务平台 + +## 二、园区官方运营数据(口径:《2026年7月运行情况统计表》) +- 实际入驻企业数:38 家(2026 年 7 月报送当月数) +- 园区可容纳企业(实体)个数:49 个 +- 开园以来累计投入园区运营资金:560 万元 +- 入驻企业累计带动就业人数(当年累计):213 人 +- 入驻企业累计生产经营总额(当年累计):252.15 万元 +- 入驻企业累计上缴税利总额(当年累计):1.03 万元 +- 开园以来累计成功孵化出园企业数:149 家 +- 当年开展活动场次:11 场;当年培训人数:362 人 +- 备注:统计表 3、9 栏为开园以来累计数,5 栏为报送当月实际入驻数,其余为当年累计数 + +## 三、省级绩效累计口径(依据《附件1.省级创业孵化载体相关情况简介材料》,截至 2026 年 5 月) +- 认定为省级创业孵化载体以来累计孵化企业:181 家;当前实有在孵创业实体:31 家 +- 团队(实体)出园数:累计 150 家(本年度 11 家) +- 入驻团队发明专利数:累计 171 项(本年度 13 项) +- 入驻团队累计经营收入:21,911.14 万元(约 2.19 亿元) +- 入驻团队累计税收缴纳:426.2 万元 +- 累计带动就业:1,529 人(本年度 106 人) +- 创业指导专家团队:50 人;运营团队:3 人 +- 开展创业指导服务活动:累计 83 场(本年度 18 场) +- 近 3 年入孵实体孵化成功率:95.91% + +## 四、入驻对象与条件(依据《入驻管理规程》) +- 入驻对象范围:在校大学生及毕业 8 年内的高校毕业生、归国留学生、外籍留学生等青年创业企业(项目);申请人须担任企业法定代表人或项目负责人;对符合昆明市产业发展的优质创业企业(项目)或 OPC 企业可适当放宽入驻条件 +- 产业方向:符合国家产业政策,聚焦昆明市「10+N」产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等) +- 其他要求:有明确可行的经营计划、启动资金和风险承担能力,具备规范的企业管理制度;无知识产权纠纷、无违法犯罪记录、无经营异常记录 +- 优先入驻情形:获得市级及以上创业大赛奖项、拥有突出学术成就、重大发明创造及高新专利技术的项目 + +## 五、入驻流程(依据《入驻管理规程》) +园区原则上每年组织两次入驻工作,可适时增减调整。流程如下: +1. 提出申请:提交《入驻申请表》《创业计划书》、创业者身份证(外籍提供护照、签证)、学历证明、企业营业执照(已注册企业)、相关资质专利证书等材料 +2. 项目初审:服务机构对申请材料初审,材料不齐的一次性告知补全 +3. 入驻评审:从《昆明市就业创业导师库》抽取 3 名评审专家,采取路演答辩方式评审,实行量化打分制(总分 100 分,80 分及以上准予通过)。评分维度:团队资质与能力(20 分)、项目创新性与技术壁垒(25 分)、产业适配与市场前景(25 分)、运营规划与落地能力(15 分)、社会价值与发展潜力(15 分) +4. 评审公示:报主管部门备案后,在昆明市人力资源和社会保障局官方网站公示,公示无异议的准予入驻 +5. 签署协议:签订《入驻孵化协议》,分配办公场地/工位,正式入驻 + +## 六、四大孵化区域与周期(依据《宣传册》《入驻管理规程》) +- OPC 创业空间:面向以人工智能技术为支撑、聚焦数字经济/AI 应用/软件开发/文创设计/电商直播等轻资产领域的项目,提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年 +- 创业苗圃区:面向创意阶段、未注册企业的大学生创业团队,提供开放式共享工位,孵化周期最长 2 年;成长好的项目经评审可推荐至孵化加速区孵化 1 年 +- 孵化加速区:面向已完成工商注册、处于初创期的企业,提供独立办公空间,基础孵化周期 2 年,期满经评估合格可申请延期 1 年,累计不超过 3 年 +- 国际创客区:面向归国留学生、外籍留学生创业项目,提供个性化办公空间及国际化配套服务,孵化周期参照孵化加速区执行 + +## 七、政策与孵化服务(依据《宣传册》) +- 空间保障:免费提供办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等 +- 孵化服务:创业指导专家一对一辅导,定期举办创业沙龙、创业讲堂、项目路演等 +- 融资支持:联动省内外创业投资机构,提供政策性贷款、商业银行贷款、股权融资等推荐指导 +- 政策对接:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策 +- 资源整合:对接驻昆高校、科研院所、龙头企业,纳入昆明创业创新服务体系,优先参与「春城创业荟」等品牌活动 +- 专项赋能培训:定期开展 AI 公益性培训(AI 技术应用、数字内容创作、一人公司创业落地、企业 AI 降本增效) +- 其他服务:工商注册、税务代办、知识产权咨询、法律援助等 + +## 八、年度考核与退出管理(依据《入驻管理规程》) +- 年度考核:量化打分制(总分 100 分),维度为经营发展能力(20)、技术创新能力(25)、合规与履约情况(20)、创业带动成效(20)、发展潜力与孵化成效(15);等级:优秀(80 分及以上)、合格(60—79 分)、不合格(60 分以下) +- 退出类型:成功孵化出园、孵化期满退出、主动申请退出、强制勒令退出 +- 强制退出情形:累计 30 个工作日无人到现场办公、连续 7 个工作日场地空置且未报备累计超 3 次、给园区造成重大经济损失或名誉损失、不服从管理情节严重、被吊销执照或破产清算、违法违规经营、私自转租或虚报数据等 + +## 九、OPC 概念(一人公司创业模式) +- OPC 即 One Person Company「一人公司」创业模式:以人工智能技术为支撑,聚焦数字经济、AI 应用、软件开发、文创设计、电商直播等轻资产领域,一人即可启动运营的创业形态 +- 园区 OPC 创业空间为 OPC 创业者提供拎包入驻的独立办公位或共享工位,孵化周期最长 2 年;园区定期开展「AI 一人公司实战课」等专项培训 +- 云南首个人工智能 OPC 创新人才基地落地昆明(2026 年 3 月公开报道),填补省内个体 AI 创业培育空白 + +## 十、入驻企业名录(依据《入驻企业信息表》39 家名单) +在园企业以《2026年7月运行情况统计表》官方口径 38 家为准;《入驻企业信息表》收录本期入驻企业(项目)名录 39 家,含云南派音人工智能科技、米勒克尔蓝宝石珠宝产业化、中泰研学合作、云南宸中低空经济、昆明智海银高文化科技、人工智能机器人大模型训练、云南廷秀文旅康养、瀚颖 AI+ 教育咨询、云南大学 AI+ 创业平台、仰光客厅、云南上古绝学文化、千楣之路、中越生物医疗、中越文旅交流、绿态环保科技、联翼航空低空经济、酷享野农 AI 农业、滇缅国际设计、昆明舒诺生物科技、达岸教育管理、昆明滇油石化、花仙子园艺肥料、Facebook 越南茶器跨境、研X 同行者网络、鬼才明 AI 创意工作室、朵哈·玫瑰特色产业链、昆明云韵体育、昆明舒护安养老服务、南菌优培食用菌、五华区丽裳文化、云南星瑞航空、云南直播私域平台、昆明屿澈电商、中外青少年研学、星杭商务信息咨询、蓝智科技、云品出滇·纸享万家、云迹轻旅、启元人工智能科技。行业分布(按简介分类):人工智能 7 家、跨境电商 5 家、教育培训 4 家、文旅康养 4 家、文化创意 4 家、生物医药 4 家、低空经济 3 家、现代农业 3 家、绿色环保 2 家、企业服务 2 家、新材料 1 家 + +## 十一、AI 平台运行指标(口径:AI 平台业务锚点,非政府统计口径) +- TOKEN 消耗:月均约 200 亿 tokens(折合日均约 6.67 亿 tokens、实时速率约 7,700 t/s)——AI 平台运行锚点测算 +- 工具调用:按平均每次工具调用约 15 万 tokens 折算,与 TOKEN 消耗实时联动 +- 实时运营数据请调用 get_park_overview 工具获取,以工具返回为准 + +## 常见问题 +- 问:如何入驻园区?答:提交申请(入驻申请表、创业计划书、身份证、学历证明等)→ 项目初审 → 入驻评审(导师库抽 3 名专家、路演答辩、80 分通过)→ 评审公示(市人社局官网)→ 签订《入驻孵化协议》、安排场地工位;每年原则上两次。 +- 问:园区有哪些政策扶持?答:空间免费、孵化辅导、融资对接、政策申报、资源整合、AI 专项培训、工商税务法律一站式服务(详见「政策与孵化服务」)。 +- 问:园区有多少家企业、带动多少就业?答:按官方统计(2026 年 7 月)实际入驻 38 家、当年累计带动就业 213 人;省级绩效累计口径(截至 2026 年 5 月)累计带动就业 1,529 人、累计经营收入约 2.19 亿元。请说明所需口径。 +- 问:什么是 OPC?答:一人公司(One Person Company)创业模式,详见「OPC 概念」。 diff --git a/backend/knowledge/park_data.md b/backend/knowledge/park_data.md new file mode 100644 index 0000000..4ebd0a0 --- /dev/null +++ b/backend/knowledge/park_data.md @@ -0,0 +1,546 @@ +# 昆明市大学生创业园 · 全量真实数据汇总 + +> 整理自 `数据资料/` 目录全部原始材料: +> 附件1.省级创业孵化载体相关情况简介材料.xlsx · 昆明市大学生创业园入驻企业信息表.xlsx · 昆明市大学生创业园运行情况统计表(2026年7月).xls · 昆明市大学生创业园创业企业(项目)入驻管理规程.wps · 昆明市创业园宣传册.pdf · 空间布局.png / 详细的布局.png · 昆明市大学生创业园 定稿(品牌设计稿) +> +> 本文件尽量保留原始表格、文件中的真实数值与原文字,供对外申报、总结汇报、宣传推广等使用。 + +--- + +## 目录 + +1. [园区基本信息](#1-园区基本信息) +2. [运营绩效核心指标(省级申报口径)](#2-运营绩效核心指标省级申报口径) +3. [2026年1—7月运行情况月统计](#3-2026年17月运行情况月统计) +4. [入驻企业完整名录(39家/信息表)](#4-入驻企业完整名录39家信息表) + - [A. 孵化加速区(加速1–10)](#a-孵化加速区) + - [B. 国际创客区(国际1–7)](#b-国际创客区) + - [C. 成长区(成长1–8)](#c-成长区) + - [D. OPC创业空间](#d-opc创业空间) + - [E. 一楼直播间](#e-一楼直播间) +5. [省级申报·入驻实体名单(31家)](#5-省级申报入驻实体名单31家) +6. [企业(项目)入驻管理规程要点](#6-企业项目入驻管理规程要点) +7. [空间布局](#7-空间布局) +8. [宣传册核心内容(园区简介·服务·政策)](#8-宣传册核心内容园区简介服务政策) +9. [入驻条件与入驻流程](#9-入驻条件与入驻流程) +10. [孵化周期(四大孵化区域)](#10-孵化周期四大孵化区域) +11. [报名方式](#11-报名方式) +12. [媒体报道与合作媒体](#12-媒体报道与合作媒体) +13. [园区新闻与活动大事记](#13-园区新闻与活动大事记) +14. [品牌VI设计规范](#14-品牌vi设计规范) + +--- + +## 1. 园区基本信息 + +| 项目 | 内容 | +|---|---| +| 载体名称 | 昆明市大学生创业园 | +| 载体类别 | 省级创业园(2012年获评) | +| 载体地址 | 云南省昆明市官渡区民航路229号(昆明市人力资源中心二楼) | +| 成立时间 | 2009年9月(2026年4月全面优化后“焕新亮相”) | +| 主管部门 | 昆明市人力资源和社会保障局 | +| 运营机构 | 云南汇浙青创园区投资有限公司(统一社会信用代码 91530112MA6PWKTUX2) | +| 申报单位 | 昆明市人力资源和社会保障局 | +| 申报单位负责人 | 黄斌(联系电话 63362979) | +| 运营负责人 | 李云鹏(联系电话 18087147411) | +| 载体特色 | 云南省首家政府主办、为创业大学生免费提供创业平台的开放式园区 | +| 主导产业 | 数字经济与电子信息产业 | +| 载体占地面积 | 3000 平方米 | +| 孵化场所面积 | 800 平方米 | +| 载体资产总值 | 400 万元 | +| 总投资(开园) | 近 100 万元 | +| 累计投入园区运营资金 | 560 万元 | +| 可容纳团队(实体)户数 | 50 户(统计表中某口径为 49 个) | +| 园区等级/称号 | 国家大学科技园体系、省级创业园等(宣传册口径:国家众创空间、国家级科技企业孵化器等称号口径不一,以正式文件为准) | + +**园区简介(300字内口径)** +昆明市大学生创业园位于昆明市民航路301号昆明人力资源中心二楼,2009年9月成立,总投资近100万元,是云南省首家免费为创业大学生提供创业平台的开放式园区,隶属于昆明市人力资源和社会保障局,由昆明市大学生创业园服务中心承担日常管理服务。园区总面积约3000平方米,可容纳50个大学生创业团队入驻,为入驻团队提供免费办公场地及配套设施,并配套政策咨询、创业规划、创业指导、创业培训、导师对接、跟踪服务、资金扶持、法律援助等全维度创业支持服务。园区坚持“高起点、高标准、高水平”办园宗旨,遵循“优质、高效、便捷”服务原则与“规范、完善、提高”管理理念,致力打造企业家培育摇篮与全省示范创业园区。截至2026年5月,园区累计孵化企业181家,其中在孵31家、成功出园149家,累计带动就业1529人、上缴利税426.2万元,38家企业累计获市级创业扶持资金149.25万元。 + +--- + +## 2. 运营绩效核心指标(省级申报口径) + +> 出处:附件1.2 省级创业孵化载体简介(运营绩效情况) + +| 指标 | 认定以来累计 | 本年度 | 当前实有 | +|---|---|---|---| +| 可容纳团队(实体)户数 | — | 50 户 | — | +| 已入驻团队(实体)户数 | 181 户 | 31 户 | 31 户 | +| 其中:应届高校毕业生(含离校2年内)参创实体数 | 114 户 | 6 户 | — | +| 团队(实体)出园数 | 150 户 | 11 户 | — | +| 返乡农民工参加创办实体数 | 0 户 | 0 户 | — | +| 入驻团队(实体)发明专利数 | 171 项 | 13 项 | — | +| 入驻团队(实体)经营收入 | 21911.14 万元 | 161.3 万元 | — | +| 入驻团队(实体)税收缴纳金额 | 426.2 万元 | 0.22 万元 | — | +| 入驻团队(实体)获得创业资金扶持金额 | 38 户 | 149.25 万元 | — | +| 孵化创业实体数 | 181 户 | — | 31 户(在孵) | +| 带动就业人数 | 1529 人 | 106 人 | — | +| 运营团队人数 | — | 3 人 | — | +| 创业指导专家团队人数 | — | 50 人 | — | +| 开展创业指导服务活动次数 | 83 场 | 18 场 | — | +| 年末孵化场所利用率 | 90%(0.9) | — | — | +| 近3年入孵实体孵化成功率 | 95.91% | — | — | +| 近3年入孵实体到期出园率 | 100%(1) | — | — | + +--- + +## 3. 2026年1—7月运行情况月统计 + +> 出处:昆明市大学生创业园运行情况统计表(2026年7月).xls —《昆明市创业平台运行情况月统计表》及《年度汇总表》 + +### 3.1 2026年7月当月快照 + +| 指标 | 数值 | +|---|---| +| 园区建筑面积 | 3000 m² | +| 开园以来累计投入园区运营资金 | 560 万元 | +| 园区可容纳企业(实体)个数 | 49 个 | +| 园区实际入驻企业数 | 38 个 | +| 入驻企业累计带动就业人数 | 213 人 | +| 入驻企业累计生产经营总额 | 252.15 万元 | +| 入驻企业累计上缴税利总额 | 1.03 万元 | +| 开园以来累计成功孵化出园企业(实体)数 | 149 户 | +| 开展活动场次 | 11 场 | +| 培训人数 | 362 人 | + +> 备注:报表第3、9栏数据为开园以来累计数,第5栏为报送当月实际入驻企业数,其他栏均为当年累计数。 + +### 3.2 2026年1—7月逐月变化(年度汇总表) + +| 月份 | 园区建筑面积(m²) | 累计运营资金(万元) | 可容纳个数 | 实际入驻企业数 | 带动就业(人) | 累计生产经营总额(万元) | 累计上缴税利(万元) | 累计出园(户) | 活动场次 | 培训人数 | +|---|---|---|---|---|---|---|---|---|---|---| +| 1月 | 700 | 510 | 18 | 15 | 73 | 28.3 | 0.04 | 139 | 0 | 0 | +| 2月 | 700 | 510 | 18 | 15 | 73 | 50.4 | 0.08 | 139 | 0 | 0 | +| 3月 | 700 | 560 | 18 | 5 | 73 | 59.1 | 0.09 | 149 | 0 | 0 | +| 4月 | 3000 | 560 | 49 | 24 | 175 | 69.73 | 0.10 | 149 | 0 | 0 | +| 5月 | 3000 | 560 | 49 | 31 | 179 | 161.3 | 0.22 | 149 | 5 | 149 | +| 6月 | 3000 | 560 | 49 | 31 | 186 | 170.4 | 0.35 | 149 | 7 | 206 | +| 7月 | 3000 | 560 | 49 | 38 | 213 | 252.15 | 1.03 | 149 | 11 | 362 | + +> 数据说明:4月起园区迁入民航路229号新场地,建筑面积由700㎡增至3000㎡、可容纳数由18个增至49个,运营资金由510万元增至560万元。截至7月底实际入驻38个,累计带动就业213人。 + +**填报信息**:单位负责人 李云鹏 / 填表人 李禹贤 / 联系电话 18087174891 / 邮箱 714066050@qq.com / 填表时间 2026年7月31日 + +--- + +## 4. 入驻企业完整名录(39家/信息表) + +> 出处:昆明市大学生创业园入驻企业信息表.xlsx(共39个项目,序号1–39,按“场地安排”列分区分布) +> 分区口径:加速=孵化加速区、国际=国际创客区、成长=成长区、OPC=OPC创业空间、一楼直播间。 + +### A. 孵化加速区 +(加速1—加速8、加速10,共9家;表中无“加速9”) + +**加速1|云南派音人工智能科技有限公司** +- 负责人:王思松,2020年毕业于应急管理大学新闻学专业。 +- 简介:OPC一人公司,2026年3月启动,入驻不足一个月即获北京相关投资支持,2026年4月正式注册成立,吸纳就业4人。核心业务主攻人工智能算法及音频模型应用研究,兼备内容创作、运营管理与技术研发综合能力,专注多模态音频技术研发。自研Pine全系列模型家族,覆盖音频识别、向量嵌入、音效生成、配乐创作及数字内容创意中枢,贯通音频理解至声音合成全链路智能。 + +**加速2|米勒克尔蓝宝石珠宝产业化(项目)** +- 负责人:王昱博,2023年3月毕业于美国加州大学圣克鲁兹分校 Multi-/Interdisciplinary 管理学专业。 +- 简介:2026年3月启动,海归留学生发起,覆盖米勒克尔大尺寸蓝宝石新材料与珠宝产业化全链条(晶体生长、材料精密加工、终端珠宝设计)。与产业链上下游深度协同,专注高品质、高附加值蓝宝石珠宝产品,推动蓝宝石新材料与珠宝产业深度融合。 + +**加速3|中泰研学合作(项目)** +- 负责人:刘泽位,泰国西那瓦大学工商管理博士二年级,昆明市侨联青年委员会副会长,精通英语、熟练泰语。 +- 简介:2026年启动运营,海归在读博士发起,围绕西那瓦大学文理学院管理运营,统筹泰国环球工商职业学院中国区事务。与中泰院校、机构深度合作,提供出国留学、中外合作办学、校企合作、产教融合及企业出海一站式服务。 + +**加速4|云南宸中低空经济有限公司** +- 负责人:王爱玲,云南大学EMBA在读,公司负责人。 +- 简介:2026年2月成立,扎根园区的创新型初创企业,专注低空经济全产业链开发运营,依托园区创业扶持体系与营商环境,快速完成项目落地与团队组建,致力于成为云南低空经济领域新兴力量。 + +**加速5|昆明智海银高文化科技有限公司** +- 负责人:宋芃燃,昆明文理学院审计学专业在读,中共党员,公司合伙人。 +- 简介:2026年4月成立,聚焦数字化财税与企业咨询服务,覆盖传统财税、AI智能财税、海外财税、财务人才培训及企业综合咨询。依托AIGC技术打造AI集成服务平台,提供财税咨询、代理记账、法律合规、企业管理、出海服务等一站式解决方案。 + +**加速6|人工智能机器人大模型训练(项目)** +- 负责人:周凯,2022年毕业于天津大学机械设计及其自动化专业。 +- 简介:2026年4月成立,聚焦人工智能机器人大模型训练,围绕大模型研发与落地提供全流程数据支撑与技术服务,开展数据采集、清洗、标注、模型训练及优化迭代,推动机器人大模型在多场景落地应用。 + +**加速7|云南廷秀文旅康养服务(项目)** +- 负责人:李雨桐,2025年毕业于日本樱美林大学老年社会学硕士。 +- 简介:负责人计划5月归国创业。立足云南“旅居云南”趋势与银发经济增长需求,解决民宿、旅居空间、康养点位在服务标准、适老能力、长期居住支持和运营转化方面的短板,建立面向银发人群的文旅旅居服务支持体系。 + +**加速8|瀚颖AI+教育信息咨询(项目)** +- 负责人:王翰啸,2019年毕业于云南大学滇池学院财务管理专业。 +- 简介:昆明瀚颖“AI+数字教育”项目2026年4月成立,由创业培训服务领域人员发起,融合AI与教育教学体系,打造集课程研发、人才培育、企业赋能、师资共建于一体的全链条数字化教育服务生态。 + +**加速10|云南大学AI+联合创业服务平台(项目)** +- 负责人:顾耘华,云南大学艺术与设计学院艺术设计在读硕士。 +- 简介:云南大学在校研究生、毕业生和创业导师共同发起的AI创业平台。以OPC社区AI人才培养与就业服务为核心,覆盖AI短视频、AI漫剧、AI+PPT制作美化等。已帮助多名大学生成功开展OPC创业;联合云南AIGC产业联盟打造“育笋计划”公益品牌,辐射大学生就创业500人+;王升雅《生态秘境》插画在《人与自然》杂志连载六期;王洋《智绘古海——AI复原寒武纪古生物》获国家级创赛金奖;杨沐晨打造西南城市鸟类多样性生态视觉品牌,承办7项生态展览;王润曦协同云南省委宣传部助力会泽白雾村打造乡村文旅振兴名片。 + +### B. 国际创客区 +(国际1—国际7,共7家) + +**国际1|仰光客厅(项目)** +- 负责人:缅甸籍留学生,毕业于云南大学经济学院。 +- 简介:缅甸留华学生发起的中缅文旅融合项目,以仰光近郊沉浸式文化营地为核心载体,面向中国客群、在缅中资机构及本地青年,集住宿、特色餐饮、文化体验、城市导览于一体,与本地社区、手工艺人合作带动就业与增收,搭建跨文化交流平台。 + +**国际2|千楣之路(项目)** +- 负责人:缅甸籍留学生,现就读于云南大学工商管理学院。 +- 简介:缅甸籍留学生创立,主营缅甸传承两千余年的传统护肤产品檀娜卡(黄香楝树干制成,缅甸已于2025年申报联合国非遗)。打造传统原木套装、便捷粉泥、现代美妆三大产品线,以“文化内容+跨境电商”模式引入中国市场。 + +**国际3|联翼航空技术低空经济应用国际化拓展(项目)** +- 负责人:杨俊蓝,2023年毕业于云南财经大学电子商务专业,瑞士商学院MSC硕士在读。 +- 简介:2026年4月启动,在读硕士发起,立足云南面向东南亚的区位门户优势,开展低空经济业务国际化拓展,在航空通航、人工智能与数字信息、教育培训方向布局,输出无人机应用、低空数据服务、跨境技能培训等内容,推动低空经济产业、人才、服务双向辐射东南亚。 + +**国际4|酷享野农AI农业(项目)** +- 负责人:晏和波,西南林业大学农业工程与信息技术专业在校研究生。 +- 简介:2026年4月启动,西南林业大学在校研究生发起,聚焦智慧农业、科研种植、农旅融合,用AI智能视觉提供农业宣传内容制作服务。与科研机构合作开展高原早熟苹果科研种植,与地方政府共建共享农庄,助力乡村振兴。 + +**国际5|滇缅国际设计(项目)** +- 负责人:张浩鹏,2022年毕业于云南财经职业学院审计学专业。 +- 简介:2026年4月启动,依托一带一路机遇,立足滇缅对外开放节点开展工程设计服务,创新跨国协同设计模式,融入绿色建造技术,覆盖交通、城乡基建等领域,与境外机构合作推动跨境项目落地。 + +**国际6|昆明舒诺生物科技有限责任公司** +- 负责人:舒岐涛,云南林业职业技术学院环境艺术专业在校生。 +- 简介:2026年4月成立,聚焦生物科技与宠物营养领域,围绕精准营养、全生命周期养护、清洁标签开展业务,以科学配方与生物活性技术打造宠物精细化营养产品。 + +**国际7|达岸教育管理(云南)有限公司** +- 负责人:吕琳,2022年毕业于泰国暹罗大学MBA研究生专业。 +- 简介:专注德国双元制教育与德国留学一站式服务(2024年启动),与德国院校合作,提供双元制培养、公立院校申请、德语培训、签证办理、境外陪护等全流程服务,帮助学生实现带薪留学、学历提升与稳定就业。 + +### C. 成长区 +(成长1—成长8,共8家) + +**成长1|花仙子园艺肥料(云南)有限公司** +- 负责人:翁淞耀,2023年毕业于云南师范大学社会体育指导与管理专业。 +- 简介:2025年4月成立,专注家庭园艺肥研发生产,针对产品异味大、品质不稳、使用不便等痛点推出安全无异味、易操作系列产品,已实现规模化生产。采用线上线下融合渠道,服务家庭养花、阳台种植民生需求。 + +**成长2|研X——同行者网络(项目)** +- 负责人:陈品祥,2024年昆明理工大学硕士毕业,兼职任教昆明冶金职业大学。 +- 简介:面向全国1621万硕博及6000万泛科研人群的一站式学术成长与资源共享平台。首创“1+6+N”生态(1个实名制平台+文献工具导航、代码共享交易、就业创业对接、学术/情感社交婚恋、科教娱乐、研究生严选商城6大模块)。已完成安卓、iOS、Windows三端开发并上架,已有全国300余所高校注册,两位高校教授以个人资金投入种子轮。 + +**成长3|朵哈·玫瑰特色产业链(项目)** +- 负责人:刘志贤,云南轻纺职业学院艺术设计专业大二在校生。 +- 简介:2026年4月启动,云南轻纺学院在校大学生发起,立足昆明安宁八街核心产区,以地标产品重瓣红玫瑰为核心原料,主打玫瑰米酿特色饮品,与省内农业产业化龙头企业合作实现原料优选、标准化生产与品质管控。 + +**成长4|昆明舒护安养老服务有限责任公司** +- 负责人:严宇佳,2019年毕业于云南艺术学院广播电视编导专业。 +- 简介:OPC一人公司,入驻园区孵化仅一个月成功吸纳就业3人。专注正规医疗护理服务,主营医院专属陪护与居家上门护理,覆盖术后康复、失能半失能照料、慢病护理、偏瘫照护、生活监护等全场景,与医疗机构深度合作。 + +**成长5|南菌优培——食用菌专用生物制剂创新与高原特色野生菌高值化全链开发(项目)** +- 负责人:王荣飞,2026年毕业于中山大学外科学专业。 +- 简介:三年以上研发基础与产业化探索,核心团队含分析化学博士、农业硕士、医学硕士等。计划2026年7月完成公司注册并启动。聚焦食用菌专用生物制剂(引入海藻糖-6-磷酸、寡糖等生物源成分)与高原特色野生菌(硬皮地星、硬皮马勃)高值化全链开发。 + +**成长6|五华区丽裳文化艺术工作室** +- 负责人:王娟,2024年毕业于昆明幼儿师范高等专科学校幼儿教育专业。 +- 简介:2026年4月成立,专注传统文化、民俗活动、茶文化及国风品牌活动策划执行与文旅研学。依托云南自贸区优势,推动民俗文创、茶产品、干花制品及高原特色产品出海,搭建“账号引流+内容传播+产品出海”运营体系。 + +**成长7|云南星瑞航空(项目)** +- 负责人:尹永浩,2019年毕业于滇西科技师范学院亚洲微电影学院数字媒体技术专业。 +- 简介:2026年4月启动,专注低空智能应用,以大疆行业级无人机为核心装备,业务涵盖无人机测绘、三维建模、数字孪生、应急救援、公共安全巡检、夜间红外搜救及电力、光伏、油气管道、森林防火等作业,并深耕文物数字化保护。具备硬件销售、数据处理、技术支持、人才培训一体化能力。 + +**成长8|综合性云南直播私域服务平台** +- 负责人:金满满,2026年7月毕业于周口师范学院学前教育专业。 +- 简介:综合性云南直播私域服务平台,深耕云南本土特色资源,打造自主IP形象,以短视频创作+全域直播运营为核心,搭建全国性自主私域流量池;同时搭建大学生创业孵化通道,构建人才迭代体系。 + +### D. OPC创业空间 +(场地安排=OPC,共6家) + +**D1|中外青少年研学(项目)**(OPC) +- 负责人:王得荣,2022年武汉大学法学专业毕业,2024—2025年英国剑桥大学短期交流学习。 +- 简介:国际游学与教育交流服务项目,已与美国、英国、澳大利亚、新加坡等多国顶尖教育机构达成长期直营战略合作,为青少年、在职教师及家长提供一站式全球化教育交流解决方案。 + +**D2|星杭商务信息咨询昆明有限责任公司** +- 负责人:杨之华,2024年6月毕业于云南师范大学教育学专业。 +- 简介:主要经营商务服务、信息咨询培训、商务接待及服务保障配套。 + +**D3|蓝智科技(项目)** +- 负责人:徐安厅,2026年毕业于昆明城市学院计算机科学与技术专业。 +- 简介:OPC一人公司,2026年5月启动,从业3人。立足西南蓝莓特色种植产业,专注农业物联网技术研发与智慧基地数字化运营,自研蓝智全系列智慧种植监控系统,涵盖田间环境监测、基质养分检测、水肥灌溉、种植生产数据管理等。 + +**D4|云品出滇·纸享万家(项目)** +- 负责人:王帅,2024年毕业于昆明理工大学电子信息工程专业。 +- 简介:2026年3月成立,以新型悬挂式抽纸为核心切入产品,依托AI工具与电商平台,打造国内电商+跨境电商双增长模式的日用纸品品牌。当前已在拼多多平台实现真实销售验证,未来向东南亚跨境电商扩展。 + +**D5|云迹轻旅——昆明本土一站式轻量化文旅服务工作室(项目)** +- 负责人:王海璇,云南大学工商管理(MBA)毕业,10年云南文旅从业经验。 +- 简介:单人OPC轻资产运营,构建“内容+服务+技术”三位一体业务矩阵:文旅自媒体IP孵化、私人小众旅行定制、非遗自然研学小团、文旅商业咨询策划、AI数字文旅技术服务五大板块。面向C端游客与B端本地文旅商家提供服务。 + +**D6|启元人工智能科技(项目)** +- 负责人:李瑞康,2017年毕业于吉林动画学院广播电视编导专业。 +- 简介:OPC一人公司,专注8-12岁儿童人工智能工具的技术推广与科普体验服务,自主研发适配低龄儿童的AI实操工具包,依托共享空间开展少儿AI创意科技体验沙龙(AI绘画、AI音乐创作、AI视频制作)。 + +> 其他在简介中表述为“OPC一人公司”但按场地安排归入其他分区的项目:云南派音人工智能科技(加速1)、昆明舒护安养老服务(成长4)、云南大学AI+联合创业服务平台(加速10)等。 +> 另:云南大学AI+联合创业服务平台完整介绍见【孵化加速区-加速10】。 + +### E. 一楼直播间 +(共1家) + +**一楼直播间|昆明屿澈电子商务有限公司** +- 负责人:杨君武,2024年6月毕业于云南交通职业技术学院建设工程管理专业。 +- 简介:高校毕业生发起的东南亚跨境电商AI智能直播+短视频出海项目,依托TikTok Shop正规跨境渠道,面向马来西亚、泰国、越南、菲律宾四国,主营日用百货、家居用品、电子配件、文创收纳、便携小家电等。采用AI数字人7×24小时无人直播、AI短视频批量引流、1688一件代发轻资产模式。 + +### F. 信息表中“场地安排”列为空的其他项目 + +《入驻企业信息表》中以下项目(共8家)的“场地安排”列未标注分区,照原文列出供全量归档: + +**云南上古绝学文化发展有限公司(简称:上古书院)** +- 负责人:赵泳淇,2024年毕业于贵州大学教育技术学专业。 +- 简介:专注上古家学文化传承与国学赋能的文化机构,以“修身齐家”为核心理念,业务涵盖家学文化课程体系研发、国学经典文化交流、家训家规个性化定制及文化研学,走“从文化体验到家族传承”路径。 + +**中越生物医疗(项目)** +- 负责人:武妙英(VŨ DIỆU ANH),越南籍留学生,云南大学工商管理专业在读,精通越语、法语、中文、英语。 +- 简介:聚焦越南标准化生物蛋白生产联合实验室建设,计划覆盖区域80%以上缺口市场,依托中越技术协同与资源互补开展高纯度生物活性蛋白合规化跨境生产与出口,满足疫苗、诊断试剂及医疗器械领域需求。已获越南五部门联合认证。 + +**中越文旅交流(项目)** +- 负责人:阮胡竹珠,云南民族大学广播电视专业在读(2027年毕业)。 +- 简介:搭建中越文化旅游交流平台,提供旅游咨询、行程定制、文化体验、语言服务及跨境旅游推广,依托广播电视专业背景与中越双语优势开展中越旅游宣传内容制作、新媒体运营、跨境文化交流活动策划。 + +**“绿态”环保科技项目** +- 负责人:陈国旗,云南民族大学在读留学生,负责海外工厂对接、中越跨境供应链统筹及中国全渠道分销网络核心运营。 +- 简介:紧抓“禁塑令”及绿色环保政策导向,专注天然槟榔叶(mo cau)环保餐具研发、跨境贸易与新媒体全渠道销售。在越南本土自有实体生产工厂,100%越南本土完成从源头集采到杀菌包装,成品经正规清关出口至中国,在昆明园区设“中国市场总分销库与营销/直播工作室”,构建“抖音直播+货架电商双驱闭环”。 + +**昆明滇油石化有限责任公司** +- 负责人:李政德,预计2027年毕业于云南国土资源职业学院地球物理勘探专业。 +- 简介:专注生物基纳米驱油剂的研发、生产与销售,为国内低渗透油藏及老油田二次/三次采油提供绿色高效增产解决方案。目标用户:中石油(华北、长庆、新疆、大庆等)、中石化(胜利、中原等)、中海油(渤海等)下属老油田;延伸至一带一路沿线国家老油田(中亚、中东、非洲)及民营油服企业(如延长石油)。 + +**Facebook越南市场普洱茶与建水紫陶壶跨境电商(项目)** +- 负责人:罗有艳,2020年毕业于云南农业大学越南语专业。 +- 简介:依托Facebook全域流量生态(主页、直播、短视频、广告),聚焦越南垂直市场,主营普洱茶、手工茶具(云南建水紫陶小品壶及公道杯组合)、中式茶礼套装跨境零售。借助“内容种草+直播转化+9610跨境报关+越南专线直邮”闭环模式。 + +**鬼才明AI创意工作室(项目)** +- 负责人:赵广明,2022年毕业于重庆大学计算机专业。 +- 简介:2026年4月启动,聚焦产教融合、创新创业赛事运营与数智化品牌赋能,围绕AI技术商业化落地、新商科专业规划、全品类活动策展及职业技能人才培育开展服务。搭建AI智能交互体系与AI数字人内容输出矩阵,为政校企提供AI场景应用、全域获客、职业技能培训、赛事集训、资格认证、品牌全案、项目申报等全链路解决方案。 + +**昆明云韵体育有限责任公司 / 男子自行车洲际队(项目)** +- 负责人:舒江贵,2020年7月毕业于麒麟区职业学校运动训练专业。 +- 简介:2026年4月发起,拟在云南注册成立UCI男子自行车洲际队,打造立足云南、辐射全国、征战亚洲赛场的职业自行车竞技项目,依托云南高原训练优势,构建“职业赛事+商业赞助+高原训练+体育文旅+青苗人才培养”一体化运营模式。 + +> 注:申报名单(第5节)将该“男子自行车洲际队项目”归类为 OPC。 + +--- + +## 5. 省级申报·入驻实体名单(31家) + +> 出处:附件1.3 创业孵化载体入驻实体名单(省级申报口径,31家) + +| 序号 | 入驻经营实体或项目团队名单 | +|---|---| +| 1 | 昆明舒护安养老服务有限责任公司 | +| 2 | 星杭商务信息咨询(昆明)有限责任公司 | +| 3 | 达岸教育管理(云南)有限公司 | +| 4 | 五华区丽裳文化艺术工作室 | +| 5 | 米勒克尔蓝宝石(项目) | +| 6 | 中越生物合作——高纯度鲎血蛋白提取的跨境技术创新和商业推广(项目) | +| 7 | 云南宸中低空经济有限公司 | +| 8 | 中泰教育合作(项目) | +| 9 | 人工智能机器人训练(项目) | +| 10 | 欧森纳东盟香港留学(项目) | +| 11 | 仰光客厅中缅文化体验营地(项目) | +| 12 | PineSound派音智能——下一代音效与配乐基座(项目) | +| 13 | 千楣之路——缅甸“千年之木”Thanaka非遗文化传播与跨境贸易(项目) | +| 14 | 昆明朵哈食品有限公司 | +| 15 | 滇缅一带一路国际化建设设计(项目) | +| 16 | 云南定影文化传播有限公司 | +| 17 | 酷享野农AI农业 | +| 18 | 昆明智海银高文化科技有限公司 | +| 19 | 鬼才明AI创意策划工作室(项目) | +| 20 | 昆明就惯你生物科技(项目) | +| 21 | 联翼航空技术低空经济应用国际化拓展(项目) | +| 22 | 中外青少年研学交流(项目) | +| 23 | 昆明瀚颖教育信息咨询有限公司 | +| 24 | 花仙子园艺肥料(云南)有限公司 | +| 25 | 云南星瑞航空科技(项目) | +| 26 | 男子自行车洲际队项目(OPC) | +| 27 | 云南廷秀文旅康养服务项目(OPC) | +| 28 | 中通乐创(项目) | +| 29 | 跨境电商出口运营项目 | +| 30 | 华寮国际教育交流(云南)有限责任公司 | +| 31 | 综合性云南直播私域服务平台 | + +> 说明:本名单项目名称部分与《入驻企业信息表》略有出入(如“米勒克尔蓝宝石”、“欧森纳东盟香港留学”、中越生物改名等),系两表不同填报口径/时点所致,均照原文保留。 + +--- + +## 6. 企业(项目)入驻管理规程要点 + +> 出处:昆明市大学生创业园创业企业(项目)入驻管理规程.wps(昆明市人力资源和社会保障局,2026年3月13日) + +### 一、实施主体 +- 运营主管:昆明市人力资源和社会保障局(主管部门) +- 执行:通过政府购买服务委托第三方服务机构(服务机构),负责入驻全流程工作;主管部门行使监督、考核、验收职权。 + +### 二、入驻条件 +- **对象范围**:在校大学生及毕业8年内高校毕业生、归国留学生、外籍留学生等青年创业企业(项目),申请人须担任法定代表人/项目负责人。符合昆明市产业发展的优质企业(项目)或OPC企业可适当放宽。 +- **产业方向**:聚焦昆明市“10+N”产业链——数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等,具备创新性、可操作性、市场前景良好。 +- **其他要求**:明确可行的经营计划、启动资金和风险承担能力,规范的企业管理制度;无知识产权纠纷、违法犯罪记录、经营异常记录;守法经营、诚实守信,遵守园区各项规定。 +- **优先入驻**:获得市级及以上创业大赛奖项、突出学术成就、重大发明创造及高新专利技术的企业(项目)。 + +### 三、入驻流程 +1. **提出申请**:提交《入驻申请表》《创业计划书》、身份证(外籍提供护照/签证)、学历证明、营业执照(已注册)、资质专利证书等。 +2. **项目初审**:服务机构初审,材料不齐一次性告知补全。 +3. **入驻评审**:服务机构从《昆明市就业创业导师库》抽取3名评审专家,路演答辩评价,量化打分制(100分),80分及以上通过。 + - 团队资质与能力 **20分**(学历背景、专业匹配度、创业经验、组织管理、人员稳定) + - 项目创新性与技术壁垒 **25分**(核心技术/商业模式创新点、自主知识产权、技术领先性、行业壁垒) + - 产业适配与市场前景 **25分**(与重点产业链匹配度、市场需求、竞品、市场拓展、盈利模式) + - 运营规划与落地能力 **15分**(商业计划完整性、启动资金、经营管理制度、阶段目标、落地方案) + - 社会价值与发展潜力 **15分**(带动就业能力、税收贡献预期、行业潜力、可持续发展) +4. **评审公示**:报主管部门备案后,在市人社局官网公示,无异议准予入驻。 +5. **签署协议**:签订《入驻孵化协议》,分配办公场地/工位,正式入驻。 + +### 四、入驻周期(梯度孵化四大区域) +| 孵化区域 | 面向对象 | 配套 | 孵化周期 | +|---|---|---|---| +| OPC创业空间 | 以AI为支撑、聚焦数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域 | 拎包入驻的独立办公位或共享工位 | 最长2年 | +| 创业苗圃区 | 创意阶段、未注册企业的大学生创业团队 | 开放式共享工位 | 最长2年(落地成长好可推荐转加速区1年) | +| 孵化加速区 | 已完成工商注册、处于初创期的企业(项目) | 独立办公空间 | 基础2年,评估合格可延1年,累计≤3年 | +| 国际创客区 | 归国留学生、外籍留学生创业企业(项目) | 个性化办公空间+国际化配套服务 | 参照孵化加速区 | + +### 五、项目年度考核 +- **考核维度**(量化打分制,100分):经营发展能力(20):营收/利润/纳税、经营计划完成度、管理制度完善度;技术创新能力(25):知识产权新增、研发投入、产品/服务迭代、产学研合作;合规与履约情况(20):遵守管理制度、协议履约、场地规范、材料报送、守法经营;创业带动成效(20):新增就业(高校毕业生占比)、实习实训岗位;发展潜力与孵化成效(15):融资进展、资质认证、市场拓展、可持续规划、出孵成熟度。 +- **考核等级**:优秀(80分及以上)、合格(60–79分)、不合格(60分以下)。 +- **结果处理**:合格准予继续孵化(期满可申请延期);不合格下达《整改通知书》,整改期满仍不合格的可解除孵化协议、勒令限期退出。 + +### 六、退出管理(四种类型) +1. **成功孵化出园**:满足任一成熟出孵标准——①产权明晰,有自主核心产品并规模化;②经营良好,产品有稳定市占率与盈利;③制度健全、组织完备、团队稳定;④已获行业经营资质、质量稳定;⑤有持续研发能力与规划。 +2. **孵化期满退出**:周期届满未申请延期或延期未通过、已延期到期。 +3. **主动申请退出**:因经营问题、团队变动等申请退出。 +4. **强制勒令退出**:①协议签订后累计30个工作日无人到现场办公,或连续7个工作日空置且未报备累计超3次;②违反规定造成重大经济损失/名誉损失;③不服从管理情节严重;④被停业、吊销执照、破产清算;⑤违法经营;⑥私自转租、变更经营项目;⑦虚报数据、伪造材料、拒不配合。 + +--- + +## 7. 空间布局 + +> 出处:空间布局.png(概念布局)与 详细的布局.png(分区布置图) + +**概念布局(空间布局.png)**:顶部左侧为“玻璃顶”(开敞中庭/天光区);上部右侧以“过道”纵贯,两侧各若干“房间”;中上部为“大厅”;中部横向依次为若干“房间”、橙色的“共享工位区”、绿色的“休息厅”;底部由完整“过道”贯通,下接一排9间“房间”。 + +**分区布置(详细的布局.png)**: + +| 区域类别 | 编号/名称 | 数量 | +|---|---|---| +| 成长区 | 成长区01–08 | 8 | +| 国际区 | 国际区01–07 | 7 | +| 加速区 | 加速区01–13 | 13 | +| 共享办公 | 共享工位区 | 1 | +| 会议空间 | 小会议室 / 大会议室 | 2 | +| 功能空间 | 玻璃顶(中庭)、休息厅、过道、楼道 | 若干 | + +> 布局总览:上部左侧为“玻璃顶”开敞区,右上依次为成长区与国际化两纵列;中部靠上为共享工位区与休息厅;下方以“过道”为轴,分布加速区、大小会议室及楼道。分区命名与《入驻企业信息表》中的“加速/国际/成长/OPC”场地安排完全对应。 + +--- + +## 8. 宣传册核心内容(园区简介·服务·政策) + +> 出处:昆明市创业园宣传册.pdf(16页彩页;以下为正文要点,图片部分略) + +### 8.1 园区定位与理念 +- 云南省首家由政府部门主办、为创业青年大学生提供**免费创业孵化服务**的**开放式园区**。 +- 2009年正式成立,昆明市人力资源和社会保障局主办运营,2012年获评省级创业园;位于民航路229号昆明市人力资源中心1–2楼,总面积约3000平方米。 +- 以助力青年大学生创业梦想为核心,构建 **“空间+孵化+融资+政策+资源+AI赋能+综合服务”七位一体**全链条创业扶持体系,打造“一站式”创业孵化服务平台。 +- 紧扣面向南亚东南亚的国际定位,融合AI前沿技术,打造现代化创业/创新服务平台,成为青年人才来昆留昆创业创新的“首站”与“家园”。 + +### 8.2 政策服务(七大板块) +1. **空间保障**:免费办公工位或独立办公区域,配备办公桌椅、文件柜、高速网络等基础办公设施。 +2. **孵化服务**:专业创业指导专家团队提供创业规划、技术指导、商业模式梳理、市场对接等“一对一”服务;定期举办创业沙龙、创业讲堂、项目路演。 +3. **融资支持**:联动省内外创投机构,设立创业种子资金/合作投资基金,提供政策性贷款、商业银行贷款、股权融资等推荐指导。 +4. **政策对接**:协助申报创业扶持资金、税收减免、社保补贴等,落实高校毕业生创业扶持政策。 +5. **资源整合**:对接驻昆高校、科研院所、龙头企业,提供技术合作、成果转化、人才招聘;纳入昆明创业创新服务体系,优先参与“春城创业荟”等品牌活动。 +6. **专项赋能培训**:定期开展AI公益性培训,提供AI应用、数字内容创作、一人公司创业落地、企业AI降本增效等公益培训与实战辅导。 +7. **其他服务**:提供工商注册、税务代办、知识产权咨询、法律援助等专业化服务。 + +--- + +## 9. 入驻条件与入驻流程 + +### 入驻条件(宣传册口径,与管理规程一致) +(一)**对象范围**:在校大学生及毕业8年内高校毕业生、归国留学生、外籍留学生等青年创业企业(项目),申请人须担任法定代表人/项目负责人;优质项目或OPC企业可放宽。 +(二)**产业方向**:聚焦昆明市“10+N”产业链(数字经济、电子信息、生物医药、先进装备、新材料、新能源、绿色食品、现代物流、文化旅游、花卉等),具备创新性、可操作性、市场前景良好。 +(三)**其他要求**:明确可行的经营计划、启动资金和风险承担能力,规范管理制度;无知识产权纠纷、违法犯罪及经营异常记录。 +(四)**优先入驻**:获得市级及以上创业大赛奖项、突出学术成就、重大发明创造及高新专利技术的项目。 + +### 入驻流程(宣传册六步) +咨询报名 → 提出申请 → 项目初审 → 入驻评审(路演答辩,量化打分≥80分通过)→ 评审公示(市人社局官微公示)→ 签署协议、分配场地工位、正式入驻。 + +--- + +## 10. 孵化周期(四大孵化区域) + +| 区域 | 对象 | 配套 | 周期 | +|---|---|---|---| +| **OPC创业空间** | AI为支撑,聚焦数字经济/AI应用/软件开发/文创设计/电商直播等轻资产领域 | 拎包入驻独立办公位或共享工位 | 最长2年 | +| **创业苗圃区** | 创意阶段、未注册企业的大学生创业团队 | 开放式共享工位 | 最长2年(成长好可转加速区1年) | +| **孵化加速区** | 已工商注册、初创期企业 | 独立办公空间 | 基础2年,评估合格延1年,累计≤3年 | +| **国际创客区** | 归国留学生、外籍留学生 | 个性化办公空间+国际化配套 | 参照孵化加速区 | + +--- + +## 11. 报名方式 + +- 咨询电话:18087174891、18214573650(宣传册);18087174891(统计表填报人);云南省就业局/园区申报口径见第3节填报信息 +- 咨询地址:昆明市民航路229号(昆明市人力资源中心二楼 昆明市大学生创业园) +- 接收邮箱:714066050@qq.com + +--- + +## 12. 媒体报道与合作媒体 + +**媒体报道矩阵**:整合电视、广播、报纸、新媒体等资源,通过电视专栏、广播专题、报纸专版、网络直播、地铁专列、新媒体推广等方式,全方位宣传报道园区创业创新典型事迹。 + +**合作媒体机构**(宣传册列示): +- 全国主流:央视网(CCTV.com)、中国网(China.org.cn)、人民网(people.cn)、今日头条、网易、搜狐、抖音 +- 省级媒体:云南日报、云南广播电视台(Yunnan Media Group)、云南网、春城晚报 +- 市级媒体:昆明广播电视台、昆明日报、掌上春城(KMCC)、无线昆明 +- 其他:8099999 新闻热线等 + +**重要外宣数据口径(宣传册/新闻引用,酌情采用)**: +- 昆明市政府网站:园区运营超过10年,入驻企业超1000家、带动创业者超1万人次(2024-04报道口径)。 +- 春城晚报:近3年累计孵化企业超100家,培育国家级高新技术企业5家、省级科技型中小企业12家。 +- 昆明日报:2024年以来新增入驻企业32家、其中高新技术企业15家;举办各类活动126场,覆盖青年创业者超2万人次。 +- **提示**:以上为媒体报道口径,与省级申报/月报表中的官方累计口径(累计孵化181家、出园149家、带动就业1529人等)存在差异,对外引用时请注明来源。 + +--- + +## 13. 园区新闻与活动大事记 + +| 时间 | 事件 | +|---|---| +| 2009年9月 | 昆明市大学生创业园成立 | +| 2012年 | 获评省级创业园 | +| 2024-04-15 | 昆明市大学生创业园2026年第二期入驻招募公告发布(同时发布第一期评审结果公示) | +| 2024-04-27前后 | 园区经全周期优化后“焕新亮相”获多家媒体报道(央视网5G、中国网、昆明市政府网、昆明日报、8099999等),报道时间多为2024-04-28 | +| 2024-05-08 | “聚力赋能·共创未来”高校创新创业交流暨项目路演活动(主办:园区;协办:滇池学院、昆明城市学院、五华区大学生创业联盟) | +| 2024-05-10 | 昆明城市学院师生赴园区“取经”参观交流 | +| 2024-05-12 | 春城晚报、云南日报整版深度报道《创业有园 未来可期》 | +| 2024-05-13 | “昆明市青年企业家协会走进园区暨创新创业基地揭牌仪式” | +| 2024-05-14 | “云南高校·昆明市大学生创业园”就业创业交流活动暨创新创业基地揭牌仪式(云南经济管理学院、昆明技师学院80余人参加) | +| 2026-03-13 | 《入驻管理规程》印发(昆明市人力资源和社会保障局) | +| 2026-03-20 | 2026年第一期入驻申请受理截止:收到申请38个,25个通过初审、13个提交复审 | +| 2026-03-24 | 第一期集中评审(30个路演参评,评审走“资格复核—现场路演—专家提问—综合评议”,量化打分) | +| 2026-03-25 | 《工作简讯〔2026〕第1期》:第一期最终确定23个优质企业(项目)正式入驻 | +| 2026-04-15 | 发布2026年第二期入驻招募公告 | +| 2026-04-24 | “创赢未来”2026创业大赛昆明选拔赛暨马兰花创业培训讲师大赛落幕(主办:昆明市人社局;承办:昆明市公共就业和人才服务中心;4月20日起4天路演,13个项目晋级省级大赛) | +| 2026-05-06 | 《工作简讯〔2026〕第2期》:此梳理第一期23家入驻企业全覆盖实地摸排,2家长期停滞企业督促整改 | +| 2026-05-20 | “滇水逐梦·雨林创航”2026昆明·西双版纳创业路演PK赛(西双版纳州公共就业和人才服务中心、滇池学院、园区联合主办;园区企业鬼才明AI活动策划工作室、云南定影文化传播有限公司协办) | +| 2026-06-03 | 云南旅游职业学院赴园区开展就业创业交流活动 | +| 近期(2026) | 云南师范大学与园区联合举办就业创业交流活动暨就业创业实践基地揭牌仪式(开启“校地产教融合、跨境双创人才共育”,缅甸、越南、印尼、老挝等国留学生参加) | + +--- + +## 14. 品牌VI设计规范 + +> 出处:昆明市大学生创业园 定稿(.ai 源文件 + 制版PNG/JPG) + +- **Logo**:橙、蓝双色组合标识——橙色“K”与深蓝“M”交织成圆环形/动感造型,英文全称“KUNMING UNIVERSITY STUDENT ENTREPRENEURSHIP PARK”,主推版式居中,品牌色橙蓝相衬。 +- **品牌主色(设计稿拾色器取值)**: + +| 色彩 | Hex | R | G | B | 说明 | +|---|---|---|---|---|---| +| 主色·橙 | `#FC8318` | 252 | 131 | 24 | 活力橙,主标识/强调 | +| 辅色·深蓝 | `#022B97` | 2 | 43 | 151 | 深邃蓝,主标识/正文 | + +- **配套文件**:`昆明市大学生创业园 定稿.ai`(矢量源文件)、`_画板 1.png/.jpg`(主画板成品)、`_画板 1 副本.png`(色彩规范参考)。 + +--- + +### 附注:数据口径说明 + +1. **累计数与当年数**:第2节绩效、第3节月报表中均区分“认定以来累计 / 本年度 / 当前实有”。本月报表中累计出园149户与省级申报累计出园150户相差1户,为两表填报时点不同所致,均照原文保留。 +2. **入驻规模**:信息表(39家)面向内部全量项目;省级申报名单(31家)为认定口径,项目名称个别有别。 +3. **宣传册数据**(媒体报道/外宣口径)与官方申报累计口径存在差异,引用时务必注明出处(见第12节提示)。 +4. 本文件所列数值均直接源自原始文件,未作推算加工。 + +--- + +*文档生成日期:2026年 · 依据 `.xlsx / .xls / .wps / .pdf / .png` 全量原始资料整理* diff --git a/backend/main.py b/backend/main.py new file mode 100644 index 0000000..6a429be --- /dev/null +++ b/backend/main.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""开发/部署入口:uv run main.py(或 python main.py) + +启动前自动完成环境初始化(无需手动 export): + 1. 读取 backend/.env(阿里云密钥、端口、MQTT 等,见 app/config.py) + 2. 本地资源收进 backend/ 内,不依赖其他项目: + TORCH_HOME -> backend/.torch-cache (silero VAD) + NLTK_DATA -> backend/nltk_data (标点/分词数据) + 3. 网络直连:SSL_CERT_FILE(certifi)、NO_PROXY(绕过系统代理直达阿里云) + 4. 默认开启 s2s 实时语音栈(.env 设 DPM_S2S_ENABLED=0 可关闭) + +对应 uvicorn 命令:uvicorn app.main:app --host 0.0.0.0 --port 10085 +""" +import os +from pathlib import Path + +BACKEND_DIR = Path(__file__).resolve().parent + + +def _load_dotenv(path: Path): + """极简 .env 解析:KEY=VALUE,支持 # 注释与引号(与 app/config.py 一致)""" + if not path.exists(): + return + for raw in path.read_text("utf-8").splitlines(): + line = raw.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, _, value = line.partition("=") + key = key.strip() + value = value.strip().strip('"').strip("'") + if key and key not in os.environ: + os.environ[key] = value + + +def prepare_env(): + """进程环境初始化 —— 必须在 import app.config / torch 之前调用。""" + # 1) 项目内 .env 优先(已有环境变量不覆盖) + _load_dotenv(BACKEND_DIR / ".env") + # 2) 本地模型/数据缓存全部收进 backend/(部署时随目录整体拷贝即可) + os.environ.setdefault("TORCH_HOME", str(BACKEND_DIR / ".torch-cache")) + os.environ.setdefault("NLTK_DATA", str(BACKEND_DIR / "nltk_data")) + # 3) 网络直连(绕过 macOS 系统 SOCKS 代理;TLS 用 certifi 证书) + os.environ.setdefault("NO_PROXY", "*") + os.environ.setdefault("no_proxy", "*") + try: + import certifi + os.environ.setdefault("SSL_CERT_FILE", certifi.where()) + except Exception: + pass + # 4) 默认开启 s2s 实时语音栈(.env 的 DPM_S2S_ENABLED=0 优先生效) + os.environ.setdefault("DPM_S2S_ENABLED", "1") + + +prepare_env() + +import uvicorn # noqa: E402 + +from app.config import settings # noqa: E402 + + +def main(): + s2s = "on" if settings.S2S_ENABLED else "off" + print("=" * 64) + print(f" DPM 后端 : http://{settings.HOST}:{settings.PORT}") + print(f" s2s 语音栈 : {s2s}" + (f" (ws://{settings.S2S_HOST}:{settings.S2S_PORT}/v1/realtime)" if settings.S2S_ENABLED else "")) + print(f" 模型 : ASR={settings.S2S_STT_MODEL} LLM={settings.S2S_LLM_MODEL} TTS={settings.S2S_TTS_MODEL} ({settings.S2S_TTS_VOICE})") + print(f" 本地资源 : TORCH_HOME={os.environ.get('TORCH_HOME')}") + print(f" NLTK_DATA={os.environ.get('NLTK_DATA')}") + print("=" * 64) + uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=False) + + +if __name__ == "__main__": + main() diff --git a/backend/models/yolov8n-face.pt b/backend/models/yolov8n-face.pt new file mode 100644 index 0000000..4f52f70 Binary files /dev/null and b/backend/models/yolov8n-face.pt differ diff --git a/backend/models/yolov8n-pose.pt b/backend/models/yolov8n-pose.pt new file mode 100644 index 0000000..f41b11f Binary files /dev/null and b/backend/models/yolov8n-pose.pt differ diff --git a/backend/pyproject.toml b/backend/pyproject.toml new file mode 100644 index 0000000..08ab44e --- /dev/null +++ b/backend/pyproject.toml @@ -0,0 +1,34 @@ +[project] +name = "backend" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "certifi>=2024.2", + "fastapi>=0.110", + "paho-mqtt>=1.6,<2", + "python-multipart>=0.0.9", + "uvicorn[standard]>=0.29", + "dashscope>=1.26", + "jinja2>=3.1", + # ---- s2s 实时语音栈(VAD本地 + 云ASR/LLM/TTS)---- + # speech-to-speech 本体以 --no-deps 安装(路径依赖见下方 [tool.uv.sources]), + # 这里只声明其运行所需的最小依赖,避免 base 依赖把 mlx/transformers 等拖进来。 + "httpx>=0.28", + "nltk==3.9.4", + "numpy>=1.26", + "openai==2.28.0", + "scipy>=1.10", + "soundfile>=0.13", + "torch>=2.4", + "transformers>=4.40", + "websockets>=12", + "ultralytics>=8.4.121", +] + +[tool.uv.sources] +# 补丁后的 s2s 云化栈,已 vendor 进项目(backend/vendor/s2s-cloud),相对路径,无跨项目依赖 +# 说明:speech-to-speech 以 `uv pip install --no-deps --no-build-isolation -e vendor/s2s-cloud` +# 手动安装(避免其 base 依赖把 mlx/transformers 等拖入);此处仅作路径声明。 +speech-to-speech = { path = "vendor/s2s-cloud" } diff --git a/backend/scripts/ingest_kb.py b/backend/scripts/ingest_kb.py new file mode 100644 index 0000000..fae015e --- /dev/null +++ b/backend/scripts/ingest_kb.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""知识库入库脚本:分块 → LLM 生成问答 → 双路向量索引 → 落盘 kb_index.json / kb_qa.json。 + +知识源 = backend/knowledge/ 下全部 *.md(park.md / opc.md / park_data.md …)。 +- 幂等:缓存有效时直接加载(不重新构建);--force 强制删除缓存重建。 +- 首次运行会调用 DashScope(text-embedding-v4 向量化 + qwen-plus 生成问答), + 耗时约数分钟,仅构建一次,之后命中缓存秒开。 + +用法(在 backend/ 目录下执行): + python scripts/ingest_kb.py # 有缓存则加载,无缓存则构建 + python scripts/ingest_kb.py --force # 强制重建(任一知识文件变更后) +""" +import argparse +import sys +from pathlib import Path + +BACKEND_DIR = Path(__file__).resolve().parent.parent +if str(BACKEND_DIR) not in sys.path: + sys.path.insert(0, str(BACKEND_DIR)) + + +def main() -> None: + parser = argparse.ArgumentParser(description="知识库入库(分块 + 问答 + 双路向量索引)") + parser.add_argument("--force", action="store_true", help="删除缓存并强制重建") + parser.add_argument("--dry", action="store_true", help="只做分块预览,不调 API、不落盘") + args = parser.parse_args() + + from app import rag + + if args.dry: + chunks = rag._chunk_md(rag._kb_text()) + print(f"[dry] 知识源 {len(list(rag.KB_DIR.glob(rag.KB_GLOB)))} 个文件,全库 " + f"{len(rag._kb_text())} 字 → {len(chunks)} 块(最长 {max(map(len, chunks))} 字)") + return + + if args.force: + for p in (rag.META_FILE, rag.VECTOR_FILE, rag.QA_FILE, rag.LEGACY_INDEX_FILE): + if p.exists(): + p.unlink() + print(f"[ingest] 已删除缓存 {p.name}") + + print("[ingest] 知识源:") + for f in sorted(rag.KB_DIR.glob(rag.KB_GLOB)): + print(f" - {f.name} ({f.stat().st_size} B)") + + idx = rag._load_index() + n_chunk = len(idx["chunks"]) + n_vec = len(idx["embed_texts"]) + dim = idx["embeddings"].shape[1] + n_q = sum(1 for qs in idx["questions"] for _ in qs) + print("[ingest] 入库完成:") + print(f" - 分块数: {n_chunk}") + print(f" - 生成问句数: {n_q}") + print(f" - 向量数: {n_vec}(dim={dim},含原句+问句双路)") + print(f" - 向量矩阵: {rag.VECTOR_FILE}") + print(f" - 元数据: {rag.META_FILE}") + print(f" - 问答缓存: {rag.QA_FILE}") + print("[ingest] 成功。") + + +if __name__ == "__main__": + main() diff --git a/backend/static/admin.css b/backend/static/admin.css new file mode 100644 index 0000000..4188d79 --- /dev/null +++ b/backend/static/admin.css @@ -0,0 +1,1267 @@ +/* ========================================================= + Admin Page Theme — 昆明市大学生创业园大屏媒体轮播 + 基于 admin.html CSS 移植 + ========================================================= */ +.admin-page { + --primary: #00BD7D; + --primary-hover: #00a36b; + --primary-glow: rgba(0, 189, 125, 0.25); + --primary-subtle: rgba(0, 189, 125, 0.07); + --primary-surface: rgba(0, 189, 125, 0.04); + --success: #16A34A; + --success-subtle: rgba(22, 163, 74, 0.08); + --warning: #D97706; + --warning-subtle: rgba(217, 119, 6, 0.08); + --danger: #DC2626; + --danger-hover: #ef4444; + --danger-subtle: rgba(220, 38, 38, 0.08); + --surface: #FFFFFF; + --text: #111827; + --text-secondary: #4b5563; + --text-dim: #9ca3af; + --text-inverse: #FFFFFF; + --border-subtle: rgba(0,0,0,0.06); + --border-default: rgba(0,0,0,0.1); + --border-strong: rgba(0,0,0,0.16); + + --space-4: 4px; + --space-8: 8px; + --space-12: 12px; + --space-16: 16px; + --space-24: 24px; + --space-32: 32px; + + --radius-sm: 4px; + --radius-md: 8px; + + --transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: 0.35s cubic-bezier(0.4, 0, 0.2, 1); + + --shadow-xs: 0 1px 2px rgba(0,0,0,0.04); + --shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04); + --shadow-card: 0 1px 3px rgba(0,0,0,0.06), 0 2px 8px rgba(0,0,0,0.04); + --shadow-card-hover: 0 4px 16px rgba(0,0,0,0.08), 0 8px 32px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.03); + --shadow-lg: 0 8px 24px rgba(0,0,0,0.1), 0 16px 48px rgba(0,0,0,0.08); + --shadow-xl: 0 12px 32px rgba(0,0,0,0.12), 0 24px 64px rgba(0,0,0,0.1); + + font-family: "Poppins", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif; + background: #f3f4f6; + color: var(--text); + min-height: 100vh; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + font-weight: 400; + position: relative; +} + +/* ============ Spatial Background ============ */ +.admin-page::before { + content: ''; + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + background: + radial-gradient(ellipse 55% 40% at 80% 10%, rgba(0, 189, 125, 0.05) 0%, transparent 55%), + radial-gradient(ellipse 50% 38% at 18% 6%, rgba(217, 119, 6, 0.04) 0%, transparent 55%), + radial-gradient(ellipse 45% 28% at 50% 96%, rgba(0,0,0,0.02) 0%, transparent 50%); +} + +.admin-page::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 0; + opacity: 0.02; + background-image: + linear-gradient(30deg, rgba(0,0,0,0.15) 1px, transparent 1px), + linear-gradient(-30deg, rgba(0,0,0,0.15) 1px, transparent 1px); + background-size: 32px 32px; +} + +/* ============ Login ============ */ +.login-wrapper { + position: relative; + z-index: 1; + display: grid; + place-items: center; + min-height: 100vh; + padding: var(--space-24); +} + +.login-wrapper::before { + content: ''; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 700px; + height: 700px; + background: + radial-gradient(circle at 50% 50%, rgba(0, 189, 125, 0.07) 0%, transparent 35%), + radial-gradient(circle at 50% 50%, rgba(217, 119, 6, 0.03) 0%, transparent 55%); + pointer-events: none; + animation: loginAmbient 8s ease-in-out infinite; +} +@keyframes loginAmbient { + 0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.7; } + 50% { transform: translate(-50%, -50%) scale(1.25); opacity: 1; } +} + +.login-wrapper::after { + content: ''; + position: fixed; + top: 10%; + right: 6%; + width: 240px; + height: 240px; + pointer-events: none; + opacity: 0.05; + background: + linear-gradient(60deg, transparent 49%, rgba(0,189,125,0.6) 49.5%, rgba(0,189,125,0.6) 50.5%, transparent 51%), + linear-gradient(-60deg, transparent 49%, rgba(0,189,125,0.6) 49.5%, rgba(0,189,125,0.6) 50.5%, transparent 51%), + linear-gradient(180deg, transparent 49%, rgba(0,189,125,0.6) 49.5%, rgba(0,189,125,0.6) 50.5%, transparent 51%); + background-size: 48px 48px; + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); +} + +.login-card { + position: relative; + background: var(--surface); + border-radius: var(--radius-md); + padding: var(--space-32); + width: 100%; + max-width: 420px; + text-align: center; + box-shadow: + 0 20px 60px rgba(0,0,0,0.08), + 0 0 0 1px rgba(0,0,0,0.04), + 0 1px 0 rgba(255,255,255,0.6) inset; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + transform: perspective(800px) rotateX(1deg); + transition: transform var(--transition-slow); +} +.login-card:hover { transform: perspective(800px) rotateX(0deg) translateY(-2px); } + +.login-card::before { + content: ''; + position: absolute; + inset: -1px; + border-radius: var(--radius-md); + padding: 1px; + background: linear-gradient(135deg, + rgba(0,0,0,0.04) 0%, + rgba(0, 189, 125, 0.2) 30%, + transparent 55%, + rgba(217, 119, 6, 0.12) 80%, + rgba(0,0,0,0.04) 100%); + -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; +} + +.login-brand { margin-bottom: var(--space-32); } +.login-brand .icon { + width: 56px; + height: 56px; + margin: 0 auto var(--space-16); + position: relative; +} +.login-brand .icon .iso-top { + position: absolute; + top: 2px; + left: 50%; + transform: translateX(-50%) rotate(45deg); + width: 30px; + height: 30px; + background: rgba(0, 189, 125, 0.12); + border: 1px solid rgba(0, 189, 125, 0.25); + border-radius: 4px; +} +.login-brand .icon .iso-left { + position: absolute; + bottom: 2px; + left: 2px; + width: 26px; + height: 24px; + background: rgba(0,0,0,0.03); + border: 1px solid rgba(0,0,0,0.08); + border-radius: 4px; + transform: skewY(25deg); +} +.login-brand .icon .iso-right { + position: absolute; + bottom: 2px; + right: 2px; + width: 26px; + height: 24px; + background: rgba(0, 189, 125, 0.15); + border: 1px solid rgba(0, 189, 125, 0.22); + border-radius: 4px; + transform: skewY(-25deg); +} +.login-brand .icon .iso-dot { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 7px; + height: 7px; + background: var(--primary); + border-radius: 50%; + box-shadow: 0 0 12px rgba(0, 189, 125, 0.35); + z-index: 2; +} +.login-brand h2 { + font-family: "Oswald", "PingFang SC", "Hiragino Sans GB", sans-serif; + font-size: 24px; + font-weight: 600; + color: var(--text); + letter-spacing: 0.02em; + text-transform: uppercase; +} +.login-brand p { + font-family: "Poppins", "PingFang SC", sans-serif; + font-size: 13px; + font-weight: 400; + color: var(--text-dim); + margin-top: var(--space-4); +} + +.login-card .field { margin-bottom: var(--space-16); text-align: left; } +.login-card .field label { + display: block; + font-family: "JetBrains Mono", "SF Mono", "Consolas", monospace; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: var(--space-8); + letter-spacing: 0.04em; + text-transform: uppercase; +} +.login-card .field input { + width: 100%; + padding: var(--space-12) var(--space-16); + background: #f9fafb; + border: 1px solid var(--border-default); + border-radius: var(--radius-sm); + font-size: 15px; + font-family: "Poppins", "PingFang SC", sans-serif; + font-weight: 400; + color: var(--text); + transition: all var(--transition); + outline: none; +} +.login-card .field input:focus { + border-color: rgba(0, 189, 125, 0.5); + box-shadow: 0 0 0 3px rgba(0, 189, 125, 0.08); +} +.login-card .field input::placeholder { color: var(--text-dim); } +.login-card .btn-login { + width: 100%; + margin-top: var(--space-8); + padding: 13px; + background: var(--primary); + color: var(--text-inverse); + border: none; + border-radius: var(--radius-sm); + font-family: "Oswald", "PingFang SC", sans-serif; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + cursor: pointer; + transition: all var(--transition); + box-shadow: 0 4px 16px rgba(0, 189, 125, 0.15); +} +.login-card .btn-login:hover { + background: var(--primary-hover); + box-shadow: 0 6px 24px rgba(0, 189, 125, 0.25); + transform: translateY(-2px); +} +.login-card .btn-login:active { transform: translateY(0); } + +/* ============ Dashboard Layout ============ */ +.dashboard { display: none; position: relative; z-index: 1; } +.dashboard.active { display: block; } + +/* ============ Topbar ============ */ +.topbar { + position: sticky; + top: 0; + z-index: 20; + background: rgba(255,255,255,0.84); + backdrop-filter: blur(24px) saturate(180%); + -webkit-backdrop-filter: blur(24px) saturate(180%); + border-bottom: 1px solid var(--border-subtle); + padding: 0 var(--space-32); + height: 64px; + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-16); + box-shadow: + 0 1px 0 rgba(255,255,255,0.5) inset, + 0 2px 16px rgba(0,0,0,0.03); +} +.topbar-left { + display: flex; + align-items: center; + gap: var(--space-12); + min-width: 0; +} +.topbar-left .brand-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--primary); + box-shadow: 0 0 10px rgba(0, 189, 125, 0.35); + flex-shrink: 0; + animation: brandDotPulse 3s ease-in-out infinite; +} +@keyframes brandDotPulse { + 0%, 100% { box-shadow: 0 0 8px rgba(0, 189, 125, 0.3); } + 50% { box-shadow: 0 0 18px rgba(0, 189, 125, 0.5); } +} +.topbar-left h1 { + font-family: "Oswald", "PingFang SC", sans-serif; + font-size: 18px; + font-weight: 600; + color: var(--text); + white-space: nowrap; + letter-spacing: 0.03em; + text-transform: uppercase; +} +.topbar-center { flex: 1; text-align: center; min-width: 0; } +.status-bar-text { + font-family: "JetBrains Mono", "SF Mono", "Consolas", monospace; + font-size: 12px; + font-weight: 500; + color: var(--text-dim); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; + max-width: 400px; + margin: 0 auto; +} +.topbar-right { + display: flex; + align-items: center; + gap: var(--space-4); + flex-shrink: 0; +} + +/* Control buttons */ +.btn-control { + width: 40px; + height: 40px; + border-radius: var(--radius-md); + border: 1px solid var(--border-subtle); + background: #f9fafb; + color: var(--text-secondary); + cursor: pointer; + font-size: 15px; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition); + flex-shrink: 0; + box-shadow: var(--shadow-xs); +} +.btn-control:hover { + background: var(--surface); + border-color: var(--border-strong); + color: var(--text); + box-shadow: var(--shadow-sm); + transform: translateY(-1px); +} +.btn-control:active { transform: translateY(1px); box-shadow: none; } +.btn-control.active { + background: var(--primary); + border-color: var(--primary); + color: var(--text-inverse); + box-shadow: 0 0 16px rgba(0, 189, 125, 0.3); +} +.btn-control.play-btn { + width: 48px; + height: 48px; + font-size: 18px; + background: var(--surface); + border-color: var(--border-default); + box-shadow: var(--shadow-card); +} +.btn-control.play-btn:hover { + background: var(--surface); + border-color: var(--border-strong); + box-shadow: var(--shadow-card-hover); +} +.btn-control.play-btn.active { + background: var(--primary); + border-color: var(--primary); + color: var(--text-inverse); + box-shadow: 0 0 20px rgba(0, 189, 125, 0.35); +} +.topbar-divider { + width: 1px; + height: 24px; + background: var(--border-subtle); + margin: 0 var(--space-4); +} +.btn-logout { + padding: var(--space-8) var(--space-16); + font-family: "Poppins", "PingFang SC", sans-serif; + font-size: 13px; + font-weight: 500; + background: transparent; + border: 1px solid var(--border-default); + color: var(--text-secondary); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} +.btn-logout:hover { + background: var(--danger-subtle); + border-color: rgba(220, 38, 38, 0.3); + color: var(--danger); + transform: translateY(-1px); +} +.btn-return { + padding: var(--space-8) var(--space-16); + font-family: "Poppins", "PingFang SC", sans-serif; + font-size: 13px; + font-weight: 500; + background: transparent; + border: 1px solid var(--border-default); + color: var(--text-secondary); + border-radius: var(--radius-md); + cursor: pointer; + transition: all var(--transition); + white-space: nowrap; +} +.btn-return:hover { + background: rgba(0, 189, 125, 0.08); + border-color: rgba(0, 189, 125, 0.3); + color: var(--primary); + transform: translateY(-1px); +} + +/* ============ Main Grid ============ */ +.main-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--space-24); + padding: var(--space-32); + max-width: 1400px; + margin: 0 auto; +} + +/* ============ Cards ============ */ +.card { + background: var(--surface); + border-radius: var(--radius-md); + padding: var(--space-24); + border: 1px solid var(--border-subtle); + box-shadow: var(--shadow-card); + transition: all var(--transition-slow); + transform: perspective(1000px) translateZ(0); + position: relative; + overflow: hidden; +} +.card::after { + content: ''; + position: absolute; + inset: 0; + border-radius: var(--radius-md); + background: linear-gradient(135deg, rgba(255,255,255,0.5) 0%, transparent 50%, rgba(0,0,0,0.01) 100%); + pointer-events: none; +} +.card:hover { + border-color: var(--border-default); + box-shadow: var(--shadow-card-hover); + transform: perspective(1000px) translateZ(4px); +} +.card.full { grid-column: 1 / -1; } +.playback-controls-card { display: none; } +.card-header { + display: flex; + align-items: center; + gap: var(--space-12); + margin-bottom: var(--space-24); + position: relative; + z-index: 1; +} +.card-header .card-icon { + font-size: 18px; + width: 36px; + height: 36px; + border-radius: var(--radius-md); + background: var(--primary-subtle); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + border: 1px solid rgba(0, 189, 125, 0.12); +} +.card-header h3 { + font-family: "Oswald", "PingFang SC", sans-serif; + font-size: 16px; + font-weight: 600; + color: var(--text); + letter-spacing: 0.02em; + text-transform: uppercase; +} + +/* ============ Form Elements ============ */ +input[type="text"], +input[type="number"], +input[type="password"], +select { + width: 100%; + padding: var(--space-12) var(--space-16); + background: #f9fafb; + border: 1px solid var(--border-default); + border-radius: var(--radius-sm); + font-family: "Poppins", "PingFang SC", sans-serif; + font-size: 14px; + font-weight: 400; + color: var(--text); + transition: all var(--transition); + outline: none; +} +input:focus, select:focus { + border-color: rgba(0, 189, 125, 0.5); + box-shadow: 0 0 0 3px rgba(0, 189, 125, 0.07); +} +input::placeholder { color: var(--text-dim); } +select { + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1L5 5L9 1' stroke='%239ca3af' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right var(--space-12) center; + padding-right: 36px; +} +input[type="file"] { + font-size: 14px; + font-family: "Poppins", "PingFang SC", sans-serif; + font-weight: 400; + color: var(--text-secondary); +} +input[type="file"]::file-selector-button { + background: #f3f4f6; + border: 1px solid var(--border-default); + border-radius: var(--radius-sm); + padding: var(--space-8) var(--space-16); + margin-right: var(--space-12); + cursor: pointer; + font-family: "Poppins", "PingFang SC", sans-serif; + font-size: 13px; + font-weight: 500; + color: var(--text); + transition: all var(--transition); +} +input[type="file"]::file-selector-button:hover { + background: var(--surface); + border-color: var(--border-strong); +} +input[type="range"] { + -webkit-appearance: none; + width: 100%; + height: 4px; + background: rgba(0,0,0,0.1); + border-radius: 2px; + padding: 0; + border: none; +} +input[type="range"]:focus { box-shadow: none; } +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + width: 20px; + height: 20px; + background: var(--primary); + border-radius: 50%; + cursor: pointer; + border: 2px solid #fff; + box-shadow: 0 1px 4px rgba(0,0,0,0.15), 0 0 8px rgba(0, 189, 125, 0.25); + transition: all var(--transition); +} +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.15); + box-shadow: 0 2px 8px rgba(0,0,0,0.2), 0 0 18px rgba(0, 189, 125, 0.35); +} + +.form-group { margin-bottom: var(--space-16); position: relative; z-index: 1; } +.form-group:last-child { margin-bottom: 0; } +.form-group label { + display: block; + font-family: "JetBrains Mono", "SF Mono", "Consolas", monospace; + font-size: 12px; + font-weight: 500; + color: var(--text-secondary); + margin-bottom: var(--space-8); + text-transform: uppercase; + letter-spacing: 0.04em; +} +.form-row { + display: flex; + gap: var(--space-8); + align-items: end; +} +.form-row > :first-child { flex: 1; } +.form-row > button { flex: 0 0 auto; } +.form-inline { + display: flex; + gap: var(--space-24); + align-items: end; + position: relative; + z-index: 1; + flex-wrap: wrap; +} +.form-inline .form-group { flex: 1; margin-bottom: 0; min-width: 0; } + +/* ============ Buttons ============ */ +button { font-family: "Poppins", "PingFang SC", sans-serif; cursor: pointer; transition: all var(--transition); } +.btn-primary { + padding: var(--space-12) var(--space-24); + border-radius: var(--radius-sm); + border: none; + font-size: 13px; + font-weight: 600; + background: var(--text); + color: var(--text-inverse); + white-space: nowrap; + letter-spacing: 0.01em; + box-shadow: var(--shadow-sm); +} +.btn-primary:hover { + background: #1f2937; + box-shadow: var(--shadow-card); + transform: translateY(-1px); +} +.btn-primary:active { transform: translateY(1px); } +.btn-accent { + padding: var(--space-12) var(--space-24); + border-radius: var(--radius-sm); + border: none; + font-size: 13px; + font-weight: 600; + background: var(--primary); + color: var(--text-inverse); + white-space: nowrap; + letter-spacing: 0.01em; + box-shadow: 0 2px 8px rgba(0, 189, 125, 0.2); +} +.btn-accent:hover { + background: var(--primary-hover); + box-shadow: 0 4px 16px rgba(0, 189, 125, 0.3); + transform: translateY(-1px); +} +.btn-accent:active { transform: translateY(1px); } +.btn-outline { + padding: var(--space-10) var(--space-20); + border-radius: var(--radius-sm); + border: 1px solid var(--border-default); + font-size: 13px; + font-weight: 500; + background: transparent; + color: var(--text-secondary); + white-space: nowrap; + cursor: pointer; + transition: all 0.2s; + letter-spacing: 0.02em; +} +.btn-outline:hover { + border-color: var(--primary); + color: var(--text); + background: rgba(0, 189, 125, 0.06); +} +.btn-outline.active { + border-color: var(--primary); + color: var(--primary); + background: rgba(0, 189, 125, 0.1); +} +.btn-danger { + padding: var(--space-12) var(--space-24); + border-radius: var(--radius-sm); + border: none; + font-size: 13px; + font-weight: 600; + background: var(--danger); + color: var(--text-inverse); + white-space: nowrap; + letter-spacing: 0.01em; + box-shadow: 0 2px 8px rgba(220, 38, 38, 0.15); +} +.btn-danger:hover { + background: var(--danger-hover); + box-shadow: 0 4px 16px rgba(220, 38, 38, 0.25); + transform: translateY(-1px); +} +.btn-sm { + padding: var(--space-8) var(--space-12); + font-size: 12px; + font-weight: 500; + border-radius: var(--radius-sm); +} + +/* ============ Media Grid ============ */ +.media-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: var(--space-16); +} +.media-card { + background: #f9fafb; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + overflow: hidden; + transition: all var(--transition-slow); + position: relative; + box-shadow: var(--shadow-xs); + transform: perspective(800px) translateZ(0); +} +.media-card:hover { + background: var(--surface); + border-color: var(--border-strong); + box-shadow: var(--shadow-card-hover); + transform: perspective(800px) translateZ(8px) translateY(-3px); +} +.media-card .thumb { + width: 100%; + height: 108px; + object-fit: cover; + background: #e5e7eb; + display: block; + cursor: pointer; +} +.thumb-video-wrap { + position: relative; + width: 100%; + height: 108px; + overflow: hidden; + cursor: pointer; + background: #111; +} +.thumb-video-wrap .thumb { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + background: #111; +} +.play-badge { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 36px; + height: 36px; + border-radius: 50%; + background: rgba(0,0,0,0.55); + color: rgba(255,255,255,0.85); + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + backdrop-filter: blur(4px); + border: 1.5px solid rgba(255,255,255,0.15); + transition: all 0.2s ease; + pointer-events: none; +} +.thumb-video-wrap:hover .play-badge { + background: rgba(0, 189, 125, 0.75); + color: #fff; + transform: translate(-50%, -50%) scale(1.1); + border-color: rgba(255,255,255,0.3); +} +.media-card .thumb-placeholder { + width: 100%; + height: 108px; + background: #f3f4f6; + display: flex; + align-items: center; + justify-content: center; + color: var(--text-dim); + font-family: "JetBrains Mono", "SF Mono", monospace; + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} +.media-card .body { padding: var(--space-12); } +.media-card .body .name { + font-family: "Poppins", "PingFang SC", sans-serif; + font-size: 13px; + font-weight: 500; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; +} +.media-card .body .meta-row { + display: flex; + gap: var(--space-4); + margin-top: var(--space-8); + flex-wrap: wrap; +} +.media-card .body .actions { + display: flex; + gap: var(--space-4); + margin-top: var(--space-12); +} +.media-card .body .actions button { flex: 1; } + +.badge { + display: inline-flex; + align-items: center; + font-family: "JetBrains Mono", "SF Mono", "Consolas", monospace; + font-size: 10px; + font-weight: 600; + padding: 2px 8px; + border-radius: var(--radius-sm); + letter-spacing: 0.04em; + text-transform: uppercase; +} +.badge-video { background: rgba(0, 189, 125, 0.1); color: #00BD7D; } +.badge-image { background: rgba(217, 119, 6, 0.1); color: #b45309; } +.badge-url { background: rgba(139, 92, 246, 0.08); color: #7c3aed; } +.badge-local { background: rgba(22, 163, 74, 0.1); color: #16A34A; } + +.empty-state { + text-align: center; + padding: var(--space-32) var(--space-24); + color: var(--text-dim); + font-family: "Poppins", "PingFang SC", sans-serif; + font-size: 14px; + font-weight: 400; + grid-column: 1 / -1; +} +.empty-state .empty-icon { font-size: 40px; margin-bottom: var(--space-12); opacity: 0.35; } + +/* ============ Toast ============ */ +.toast-container { + position: fixed; + top: var(--space-24); + right: var(--space-24); + z-index: 9999; + display: flex; + flex-direction: column; + gap: var(--space-8); + pointer-events: none; +} +.toast { + padding: var(--space-12) var(--space-24); + border-radius: var(--radius-md); + font-family: "Poppins", "PingFang SC", sans-serif; + font-size: 13px; + font-weight: 500; + pointer-events: auto; + backdrop-filter: blur(24px) saturate(180%); + -webkit-backdrop-filter: blur(24px) saturate(180%); + animation: toastIn 0.35s cubic-bezier(0.16, 1, 0.3, 1); + max-width: 360px; + border: 1px solid; + box-shadow: var(--shadow-lg); +} +.toast.success { background: rgba(22, 163, 74, 0.1); color: #166534; border-color: rgba(22, 163, 74, 0.2); } +.toast.error { background: rgba(220, 38, 38, 0.08); color: #991b1b; border-color: rgba(220, 38, 38, 0.18); } +@keyframes toastIn { + from { opacity: 0; transform: translateX(40px) scale(0.92); } + to { opacity: 1; transform: translateX(0) scale(1); } +} + +/* ============ Preview Modal ============ */ +.modal-overlay { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0,0,0,0.45); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-32); + animation: overlayIn 0.25s ease; +} +@keyframes overlayIn { from { opacity: 0; } to { opacity: 1; } } +.modal-overlay.closing { animation: overlayOut 0.2s ease forwards; } +@keyframes overlayOut { from { opacity: 1; } to { opacity: 0; } } +.modal-panel { + position: relative; + max-width: 90vw; + max-height: 85vh; + width: auto; + background: var(--surface); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + overflow: hidden; + box-shadow: var(--shadow-xl); + animation: panelIn 0.35s cubic-bezier(0.16, 1, 0.3, 1); + transform: perspective(1000px) rotateX(0.5deg); +} +@keyframes panelIn { from { opacity: 0; transform: perspective(1000px) rotateX(2deg) translateY(20px) scale(0.94); } to { opacity: 1; transform: perspective(1000px) rotateX(0.5deg) translateY(0) scale(1); } } +.modal-overlay.closing .modal-panel { animation: panelOut 0.2s ease forwards; } +@keyframes panelOut { from { opacity: 1; transform: perspective(1000px) rotateX(0.5deg) translateY(0) scale(1); } to { opacity: 0; transform: perspective(1000px) rotateX(2deg) translateY(20px) scale(0.94); } } +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-16) var(--space-24); + border-bottom: 1px solid var(--border-subtle); + background: #f9fafb; +} +.modal-header .modal-title { + font-family: "Oswald", "PingFang SC", sans-serif; + font-size: 14px; + font-weight: 600; + color: var(--text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-right: var(--space-12); + letter-spacing: 0.02em; + text-transform: uppercase; +} +.modal-close { + width: 32px; + height: 32px; + border-radius: var(--radius-md); + border: 1px solid var(--border-subtle); + background: transparent; + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + font-size: 16px; + transition: all var(--transition); +} +.modal-close:hover { + background: var(--danger-subtle); + border-color: rgba(220, 38, 38, 0.3); + color: var(--danger); +} +.modal-body { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-24); + background: #f3f4f6; + min-height: 200px; + max-height: 65vh; + overflow: auto; +} +.modal-body img { max-width: 100%; max-height: 60vh; object-fit: contain; border-radius: var(--radius-sm); } +.modal-body video { max-width: 100%; max-height: 60vh; border-radius: var(--radius-sm); outline: none; } +.modal-footer { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--space-12) var(--space-24); + border-top: 1px solid var(--border-subtle); + background: #f9fafb; +} +.modal-info { + font-family: "JetBrains Mono", "SF Mono", "Consolas", monospace; + font-size: 11px; + font-weight: 500; + color: var(--text-dim); + display: flex; + gap: var(--space-4); + align-items: center; +} +.modal-actions { display: flex; gap: var(--space-8); } + +/* ============ Utilities ============ */ +.hidden { display: none !important; } +.volume-label { + font-family: "JetBrains Mono", "SF Mono", "Consolas", monospace; + font-size: 12px; + font-weight: 500; + color: var(--text-dim); + text-align: right; + margin-top: var(--space-4); + font-variant-numeric: tabular-nums; +} + +/* ============ Upload Progress ============ */ +.upload-progress { + margin-top: var(--space-12); + display: flex; + align-items: center; + gap: var(--space-12); +} +.progress-bar { + flex: 1; + height: 6px; + background: rgba(0,0,0,0.06); + border-radius: 3px; + overflow: hidden; +} +.progress-fill { + height: 100%; + background: var(--primary); + border-radius: 3px; + transition: width 0.25s ease; + box-shadow: 0 0 8px rgba(0, 189, 125, 0.3); +} +.progress-text { + font-family: "JetBrains Mono", "SF Mono", "Consolas", monospace; + font-size: 11px; + font-weight: 500; + color: var(--text-dim); + white-space: nowrap; + flex-shrink: 0; + font-variant-numeric: tabular-nums; +} + +/* ============ Responsive ============ */ +@media (max-width: 768px) { + .login-card { padding: var(--space-24) var(--space-16) var(--space-32); margin: var(--space-12); max-width: none; } + .login-brand h2 { font-size: 20px; } + .topbar { padding: 0 var(--space-16); height: 56px; gap: var(--space-4); } + .topbar-left h1 { font-size: 14px; } + .topbar-center { display: none; } + .topbar-right { gap: var(--space-2); } + /* Hide playback controls in topbar on mobile — they're in their own card */ + .topbar-right .btn-control, + .topbar-right .play-btn, + .topbar-right .topbar-divider { display: none; } + .btn-logout, .btn-return { padding: var(--space-4) var(--space-10); font-size: 11px; } + .main-grid { grid-template-columns: 1fr; gap: var(--space-12); padding: var(--space-12); } + .card { padding: var(--space-12) var(--space-16); } + .card.full { grid-column: 1; } + .card-header { margin-bottom: var(--space-16); } + .card-header h3 { font-size: 14px; } + .card-header .card-icon { width: 30px; height: 30px; } + .card-header .card-icon svg { width: 16px; height: 16px; } + .form-inline { flex-direction: column; gap: var(--space-12); align-items: stretch; } + .form-group { margin-bottom: var(--space-12); } + .form-group label { font-size: 11px; } + .media-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-8); } + .media-card .thumb { height: 88px; } + .thumb-video-wrap { height: 88px; } + .media-card .body { padding: var(--space-8); } + .media-card .body .name { font-size: 12px; } + .media-card .body .actions button { font-size: 11px; padding: var(--space-4) var(--space-8); } + .toast-container { top: var(--space-12); right: var(--space-12); left: var(--space-12); } + .toast { max-width: none; font-size: 12px; } + .btn-primary, .btn-accent, .btn-danger { padding: var(--space-12) var(--space-16); font-size: 12px; } + .btn-outline { padding: var(--space-8) var(--space-14); font-size: 12px; } + .modal-overlay { padding: var(--space-16); } + .modal-panel { max-width: 100%; max-height: 90vh; } + .modal-body { padding: var(--space-12); max-height: 55vh; } + .modal-body img, .modal-body video { max-height: 50vh; } + .modal-header { padding: var(--space-12) var(--space-16); } + .modal-footer { padding: var(--space-8) var(--space-16); } + + /* Mobile playback control card */ + .playback-controls-card { display: block; } + .playback-controls-row { + display: flex; + align-items: center; + justify-content: center; + gap: var(--space-8); + padding: var(--space-4) 0; + } + .playback-controls-row .btn-control { + width: 44px; + height: 44px; + border-radius: var(--radius-md); + border: 1px solid var(--border-subtle); + background: #f9fafb; + color: var(--text-secondary); + cursor: pointer; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + transition: all var(--transition); + flex-shrink: 0; + box-shadow: var(--shadow-xs); + } + .playback-controls-row .btn-control:hover { + background: var(--surface); + border-color: var(--border-strong); + color: var(--text); + box-shadow: var(--shadow-sm); + transform: translateY(-1px); + } + .playback-controls-row .btn-control.active { + background: var(--primary); + border-color: var(--primary); + color: var(--text-inverse); + box-shadow: 0 0 16px rgba(0, 189, 125, 0.3); + } + .playback-controls-row .play-btn { + width: 52px; + height: 52px; + font-size: 20px; + } + .playback-controls-row .btn-control:active { transform: translateY(1px); } +} + +@media (max-width: 400px) { + .topbar-left h1 { font-size: 12px; } + .topbar-right { gap: 2px; } + .topbar-right .btn-logout, .topbar-right .btn-return { font-size: 10px; padding: var(--space-2) var(--space-8); } + .main-grid { gap: var(--space-8); padding: var(--space-8); } + .card { padding: var(--space-12); } + .media-grid { grid-template-columns: repeat(2, 1fr); gap: var(--space-4); } + .playback-controls-row { gap: var(--space-4); } + .playback-controls-row .btn-control { width: 38px; height: 38px; font-size: 14px; } + .playback-controls-row .play-btn { width: 44px; height: 44px; font-size: 17px; } + .form-inline { gap: var(--space-8); align-items: stretch; } + .form-group { margin-bottom: var(--space-8); } +} + +/* ========================================================= + 管理后台增强:页面切换 / 立即播放 / URL 展示 + ========================================================= */ +.page-switch-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; +} +.page-switch-btn { + padding: 12px 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary, #00BD7D); + background: var(--primary-subtle, rgba(0,189,125,0.07)); + border: 1px solid rgba(0, 189, 125, 0.28); + border-radius: 10px; + cursor: pointer; + transition: all 0.2s; +} +.page-switch-btn:hover { + background: rgba(0, 189, 125, 0.14); + transform: translateY(-1px); +} +.screen-target.active { + background: #1D5DCE; + border-color: #1D5DCE; + color: #fff; + box-shadow: 0 4px 12px rgba(29, 93, 206, 0.3); +} +.screen-target.active:hover { background: #174fb0; } +.media-url { + display: flex; + align-items: center; + gap: 6px; + margin: 4px 0 6px; + padding: 3px 6px; + min-width: 0; + background: rgba(0, 189, 125, 0.05); + border: 1px solid rgba(0, 189, 125, 0.14); + border-radius: 6px; +} +.media-url span { + flex: 1; + min-width: 0; + font-size: 10px; + font-family: "SF Mono", "JetBrains Mono", Menlo, monospace; + color: #6b7280; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.copy-btn { + flex-shrink: 0; + padding: 2px 7px; + font-size: 10px; + border: none; + border-radius: 5px; + background: rgba(0, 189, 125, 0.14); + color: var(--primary, #00BD7D); + cursor: pointer; + transition: all 0.2s; +} +.copy-btn:hover { background: rgba(0, 189, 125, 0.24); } +.media-card .actions { + flex-wrap: wrap; + gap: 5px; +} +@media (max-width: 900px) { + .page-switch-grid { grid-template-columns: repeat(2, 1fr); } +} + +/* ========================================================= + 连接状态卡片 + ========================================================= */ +.conn-status { + display: flex; + flex-direction: column; + gap: 6px; +} +.conn-row { + display: flex; + align-items: center; + gap: 8px; + padding: 7px 10px; + background: rgba(0, 189, 125, 0.04); + border: 1px solid rgba(0, 189, 125, 0.12); + border-radius: 8px; +} +.conn-dot { + width: 8px; + height: 8px; + flex-shrink: 0; + border-radius: 50%; + background: #d1d5db; +} +.conn-dot.on { + background: #16A34A; + box-shadow: 0 0 6px rgba(22, 163, 74, 0.7); + animation: connPulse 1.6s ease-in-out infinite; +} +.conn-dot.off { + background: #DC2626; + box-shadow: 0 0 6px rgba(220, 38, 38, 0.6); +} +@keyframes connPulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} +.conn-label { + flex-shrink: 0; + font-size: 12px; + color: var(--text-secondary, #4b5563); + width: 72px; +} +.conn-value { + flex: 1; + min-width: 0; + font-size: 12px; + font-weight: 600; + color: var(--text, #111827); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ============ 大屏控制(MQTT 指令)面板 ============ */ +.mqtt-group { margin: 10px 0 4px; } +.mqtt-group-title { + font-size: 12px; font-weight: 600; color: #5a6b8c; + margin-bottom: 6px; letter-spacing: 0.04em; +} +.mqtt-row { + display: flex; flex-wrap: wrap; gap: 6px; align-items: center; + margin-bottom: 6px; +} +.mqtt-input { + flex: 1; min-width: 180px; + padding: 6px 10px; border: 1px solid #d7e1f0; border-radius: 8px; + font-size: 13px; color: #1d2c44; outline: none; +} +.mqtt-input:focus { border-color: #2f6bff; } diff --git a/backend/static/admin.js b/backend/static/admin.js new file mode 100644 index 0000000..5bd9e9f --- /dev/null +++ b/backend/static/admin.js @@ -0,0 +1,447 @@ +/* ========================================================= + Admin 管理后台前端逻辑(Jinja 页面配套) + 事件采用 document 委托绑定,保证按钮点击稳定响应; + 连接状态每 3s 轮询 /api/display/state 刷新 + ========================================================= */ +(function () { + 'use strict'; + + var init = window.__DPM_INIT || { fullscreen: true, autostart: false }; + var fullscreen = !!init.fullscreen; + var autostart = !!init.autostart; + + /* 目标屏幕(按 MQTT 客户端精准控制):client_id + role */ + var screenId = ''; + var screenRole = ''; + + function applyScreenTarget(sel) { + sel = sel || document.getElementById('screenTarget'); + if (!sel) return; + var v = sel.value || ''; + screenId = v; + var opt = null; + if (v) { for (var i = 0; i < sel.options.length; i++) { if (sel.options[i].value === v) { opt = sel.options[i]; break; } } } + screenRole = opt ? (opt.getAttribute('data-role') || '') : ''; + } + + function loadScreens() { + fetch('/api/display/state', { cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (d) { + var sel = document.getElementById('screenTarget'); + var cnt = document.getElementById('screenCount'); + if (!sel) return; + var devices = (d && d.devices) || []; + if (cnt) cnt.textContent = '在线设备:' + (devices.length || 0) + ' 台'; + var cur = sel.value; + sel.innerHTML = ''; + devices.forEach(function (dev) { + var role = dev.role || ''; + var name = role === 'main' ? '主屏' : role === 'secondary' ? '副屏' : (role || '屏幕'); + var parent = (dev.role === 'secondary' && dev.parent_id) ? '·属于:' + String(dev.parent_id).slice(0, 8) : ''; + var opt = document.createElement('option'); + opt.value = dev.device_id; + opt.setAttribute('data-role', role); + opt.textContent = name + parent + ' · ' + String(dev.device_id).slice(0, 16); + sel.appendChild(opt); + }); + if (cur) sel.value = cur; + applyScreenTarget(sel); + }) + .catch(function () {}); + } + + /* ---------- Toast ---------- */ + var toastContainer = document.getElementById('toastContainer'); + function toast(msg, type) { + if (!toastContainer) return; + var el = document.createElement('div'); + el.className = 'toast ' + (type || 'success'); + el.textContent = msg; + toastContainer.appendChild(el); + setTimeout(function () { el.remove(); }, 2800); + } + + /* ---------- 通用请求 ---------- */ + function post(url, body, cb) { + fetch(url, { + method: 'POST', + headers: body ? { 'Content-Type': 'application/json' } : undefined, + body: body ? JSON.stringify(body) : undefined, + }) + .then(function (r) { return r.json().catch(function () { return {}; }); }) + .then(function (d) { + if (d && d.ok === false) { toast(d.error || '操作失败', 'error'); return; } + if (cb) cb(d); + }) + .catch(function () { toast('网络错误', 'error'); }); + } + function reload(msg, delay) { + if (msg) toast(msg); + setTimeout(function () { location.reload(); }, delay || 600); + } + + /* ---------- 播放状态(SSE,仅更新状态文字,不自动重载页面) ---------- */ + var statusText = document.getElementById('statusText'); + try { + var es = new EventSource('/api/events'); + es.onmessage = function (e) { + var msg; try { msg = JSON.parse(e.data); } catch (err) { return; } + if (msg.action === 'state_update' && msg.state && statusText) { + var s = msg.state; + if (!s.status || s.status === 'idle' || !s.name) statusText.textContent = '等待大屏连接...'; + else if (s.status === 'playing') statusText.textContent = '正在播放 — ' + s.name; + else if (s.status === 'paused') statusText.textContent = '已暂停 — ' + s.name; + } + }; + } catch (err) { /* ignore */ } + + /* ========================================================= + 连接状态轮询(/api/display/state) + ========================================================= */ + var connEls = { + api: document.getElementById('connApi'), + mqtt: document.getElementById('connMqtt'), + screens: document.getElementById('connScreens'), + lastCmd: document.getElementById('connLastCmd'), + }; + function setConn(id, ok, value) { + var el = connEls[id]; + if (!el) return; + var dot = el.querySelector('.conn-dot'); + var val = el.querySelector('.conn-value'); + if (dot) dot.className = 'conn-dot ' + (ok ? 'on' : 'off'); + if (val) val.textContent = value; + } + function pollStatus() { + fetch('/api/display/state', { cache: 'no-store' }) + .then(function (r) { return r.json(); }) + .then(function (d) { + setConn('api', true, '在线'); + setConn('mqtt', !!d.mqtt_connected, d.mqtt_connected ? '已连接(' + (d.mqtt_host || '') + ')' : '未连接'); + setConn('screens', (d.screens_online || 0) > 0, (d.screens_online || 0) + ' 台'); + loadScreens(); // 刷新目标屏幕下拉(按 MQTT 客户端) + var lc = d.last_command; + if (lc) { + var names = { navigate: '页面切换', play: '播放', pause: '暂停', next: '下一项', prev: '上一项', + 'play_target': '指定播放', alert: '通知', show_card: '卡片', set_mode: '播放模式' }; + var when = new Date(lc.ts).toLocaleTimeString('zh-CN', { hour12: false }); + setConn('lastCmd', !!lc.published, + (names[lc.action] || lc.action) + ' · ' + (lc.published ? '已广播' : '未广播') + ' · ' + when); + } + }) + .catch(function () { + setConn('api', false, '离线'); + setConn('mqtt', false, '未知'); + setConn('screens', false, '未知'); + }); + } + if (connEls.api) { pollStatus(); setInterval(pollStatus, 3000); } + // 目标屏幕下拉:变更时记录目标 client_id + role;初始加载一次 + var screenSel = document.getElementById('screenTarget'); + if (screenSel) { + screenSel.addEventListener('change', function () { applyScreenTarget(screenSel); }); + loadScreens(); + } + + /* ========================================================= + 事件委托:页面切换 / 播放控制 / 媒体操作 + ========================================================= */ + // 输入框回车提交 + document.addEventListener('keydown', function (e) { + if (e.key !== 'Enter') return; + if (e.target && e.target.id === 'mqttAiInput') { document.getElementById('mqttAiSend').click(); } + else if (e.target && e.target.id === 'mqttAlertText') { document.getElementById('mqttAlertSend').click(); } + }); + + document.addEventListener('click', function (e) { + var btn = e.target.closest ? e.target.closest('[data-nav], [data-mqtt], [data-control], [data-action], .copy-btn, [data-preview], #mqttAiSend, #mqttAlertSend') : null; + if (!btn) return; + e.stopPropagation(); + + // 预览 + if (btn.hasAttribute('data-preview')) { openPreview(btn.closest('.media-card')); return; } + + // 复制 URL + if (btn.classList.contains('copy-btn')) { + var url = btn.closest('.media-url').getAttribute('data-url') || ''; + var full = /^https?:\/\//.test(url) ? url : window.location.origin + url; + var done = function () { toast('链接已复制'); }; + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(full).then(done).catch(function () { fallbackCopy(full); done(); }); + } else { fallbackCopy(full); done(); } + return; + } + + // 大屏 MQTT 指令(data-mqtt 携带完整 {action, params} JSON) + if (btn.hasAttribute('data-mqtt')) { + var cmd = null; + try { cmd = JSON.parse(btn.getAttribute('data-mqtt')); } catch (err) { toast('指令 JSON 错误', 'error'); return; } + // 屏幕终端精准控制:除「双屏开关」外,注入目标 client_id + role;全空=全局广播 + if (cmd.action !== 'dual_screen') { cmd.screen_id = screenId; cmd.screen_role = screenRole; } + var tgtTxt = screenId + ? (cmd.screen_role === 'main' ? '主屏' : cmd.screen_role === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10) + : '全部'; + post('/api/display/command', cmd, function (d) { + toast(d && d.ok !== false ? ('指令已发送:' + (cmd.action || '') + ' → ' + tgtTxt) : '发送失败(MQTT 未连接?)', + d && d.ok !== false ? 'ok' : 'error'); + }); + return; + } + + // AI 提问输入框 + if (btn.id === 'mqttAiSend') { + var text = (document.getElementById('mqttAiInput').value || '').trim(); + if (!text) { toast('请输入问题', 'error'); return; } + post('/api/display/command', { action: 'ai_input', params: { text: text }, screen_id: screenId, screen_role: screenRole }, function () { + toast('已发送问题到大屏 AI 页'); + }); + return; + } + + // 通知输入框 + if (btn.id === 'mqttAlertSend') { + var alertText = (document.getElementById('mqttAlertText').value || '').trim(); + if (!alertText) { toast('请输入通知内容', 'error'); return; } + post('/api/display/command', { action: 'alert', params: { text: alertText }, screen_id: screenId, screen_role: screenRole }, function () { + toast('通知已发送'); + }); + return; + } + + // 页面切换(绑定目标屏幕:screen_id 精确某台;空=全部) + if (btn.hasAttribute('data-nav')) { + var page = btn.getAttribute('data-nav'); + var tgtTxt = screenId + ? (screenRole === 'main' ? '主屏' : screenRole === 'secondary' ? '副屏' : '屏幕') + '·' + String(screenId).slice(0, 10) + : '全部'; + post('/api/display/command', { action: 'navigate', params: { page: page }, screen_id: screenId, screen_role: screenRole }, function () { + toast('已切换大屏至「' + btn.textContent.trim() + '」 → ' + tgtTxt); + }); + return; + } + + // 播放控制 + if (btn.hasAttribute('data-control')) { + var action = btn.getAttribute('data-control'); + if (action === 'minimize') post('/api/display/command', { action: 'minimize', params: {} }); + else post('/api/control', { action: action }); + return; + } + + // 媒体操作 + if (btn.hasAttribute('data-action')) { + var card = btn.closest('.media-card'); + var path = card.getAttribute('data-path'); + var act = btn.getAttribute('data-action'); + if (act === 'play-now') { + post('/api/playlist/play', { path: path }, function () { toast('已发送播放指令,大屏即将播放'); }); + } else if (act === 'add-playlist') { + post('/api/playlist/add', { path: path }, function () { reload('已加入播放列表'); }); + } else if (act === 'remove-playlist') { + post('/api/playlist/remove', { path: path }, function () { reload('已移出播放列表'); }); + } else if (act === 'delete') { + if (!window.confirm('确定删除该文件?')) return; + post('/api/delete', { path: path }, function () { reload('已删除'); }); + } + } + }); + + function fallbackCopy(text) { + var ta = document.createElement('textarea'); + ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; + document.body.appendChild(ta); ta.select(); + try { document.execCommand('copy'); } catch (err) { /* ignore */ } + document.body.removeChild(ta); + } + + /* ========================================================= + 播放设置(change/input 事件) + ========================================================= */ + var playMode = document.getElementById('playMode'); + if (playMode) playMode.addEventListener('change', function () { + post('/api/settings', { play_mode: playMode.value }, function () { toast('播放模式已更新'); }); + }); + + var imageDuration = document.getElementById('imageDuration'); + if (imageDuration) imageDuration.addEventListener('change', function () { + var v = Number(imageDuration.value); + if (v < 1 || v > 300) { toast('时长需在 1-300 秒', 'error'); imageDuration.value = 5; return; } + post('/api/settings', { image_duration: v }, function () { toast('图片时长已更新'); }); + }); + + var volumeRange = document.getElementById('volumeRange'); + var volumeLabel = document.getElementById('volumeLabel'); + var volTimer = null; + if (volumeRange && volumeLabel) { + volumeRange.addEventListener('input', function () { + volumeLabel.textContent = volumeRange.value + '%'; + if (volTimer) clearTimeout(volTimer); + volTimer = setTimeout(function () { + post('/api/settings', { volume: Number(volumeRange.value) }, function () { toast('媒体音量已更新'); }); + }, 300); + }); + } + + var sfxVolumeRange = document.getElementById('sfxVolumeRange'); + var sfxVolumeLabel = document.getElementById('sfxVolumeLabel'); + var sfxVolTimer = null; + if (sfxVolumeRange && sfxVolumeLabel) { + sfxVolumeRange.addEventListener('input', function () { + sfxVolumeLabel.textContent = sfxVolumeRange.value + '%'; + if (sfxVolTimer) clearTimeout(sfxVolTimer); + sfxVolTimer = setTimeout(function () { + post('/api/settings', { sfx_volume: Number(sfxVolumeRange.value) }, function () { toast('音效音量已更新'); }); + }, 300); + }); + } + + /* ---------- 显示控制 ---------- */ + var btnFullscreen = document.getElementById('btnFullscreen'); + var btnAutostart = document.getElementById('btnAutostart'); + function renderDisplay() { + if (btnFullscreen) btnFullscreen.textContent = fullscreen ? '切换窗口模式' : '切换大屏模式'; + if (btnAutostart) { btnAutostart.textContent = autostart ? '已开启' : '已关闭'; btnAutostart.classList.toggle('active', autostart); } + } + if (btnFullscreen) btnFullscreen.addEventListener('click', function () { + fullscreen = !fullscreen; renderDisplay(); + post('/api/settings', { fullscreen: fullscreen }); + }); + if (btnAutostart) btnAutostart.addEventListener('click', function () { + autostart = !autostart; renderDisplay(); + post('/api/settings', { autostart: autostart }); + }); + renderDisplay(); + + /* ---------- 上传(带进度) ---------- */ + var btnUpload = document.getElementById('btnUpload'); + if (btnUpload) btnUpload.addEventListener('click', function () { + var inp = document.getElementById('fileInput'); + var fileList = Array.from(inp.files || []); + if (fileList.length === 0) { toast('请选择文件', 'error'); return; } + var prog = document.getElementById('uploadProgress'); + var fill = document.getElementById('progressFill'); + var text = document.getElementById('progressText'); + prog.style.display = 'block'; + var done = 0; + fileList.forEach(function (file) { + var fd = new FormData(); + fd.append('file', file); + var xhr = new XMLHttpRequest(); + xhr.open('POST', '/upload'); + xhr.upload.onprogress = function (e) { + if (e.lengthComputable) { + var pct = Math.round((e.loaded / e.total) * 100); + var overall = Math.round(((done + e.loaded / e.total) / fileList.length) * 100); + fill.style.width = overall + '%'; + text.textContent = file.name + ' (' + (done + 1) + '/' + fileList.length + ') — ' + pct + '%'; + } + }; + xhr.onload = function () { + done += 1; + if (done === fileList.length) reload('上传完成'); + }; + xhr.onerror = function () { toast(file.name + ' 上传失败', 'error'); done += 1; }; + xhr.send(fd); + }); + }); + + /* ---------- 远程 URL ---------- */ + var btnAddUrl = document.getElementById('btnAddUrl'); + var btnAddUrlPlay = document.getElementById('btnAddUrlPlay'); + function addUrlThen(play) { + var inp = document.getElementById('urlInput'); + var url = (inp.value || '').trim(); + if (!url) { toast('请输入 URL', 'error'); return; } + post('/api/media/add-url', { url: url }, function (d) { + if (play) { + post('/api/playlist/play', { path: url }, function () { toast('已加入并发送播放指令'); }); + } else { + reload(d && d.duplicate ? 'URL 已存在于媒体库' : '已添加到媒体库'); + } + }); + } + if (btnAddUrl) btnAddUrl.addEventListener('click', function () { addUrlThen(false); }); + if (btnAddUrlPlay) btnAddUrlPlay.addEventListener('click', function () { addUrlThen(true); }); + + /* ========================================================= + 预览弹窗 + ========================================================= */ + var modal = document.getElementById('previewModal'); + var modalBody = document.getElementById('previewBody'); + var current = null; + + function openPreview(card) { + if (!card || !modal) return; + var path = card.getAttribute('data-path'); + var type = card.getAttribute('data-type'); + var source = card.getAttribute('data-source'); + var name = card.getAttribute('data-name'); + var url = source === 'url' ? path : '/file/' + encodeURI(path); + current = { path: path, url: url, type: type, name: name }; + document.getElementById('previewTitle').textContent = name; + modalBody.innerHTML = type === 'image' + ? '' + name + '' + : ''; + document.getElementById('previewMeta').innerHTML = + '' + (source === 'url' ? '远程' : '本地') + '' + + '' + (type === 'video' ? '视频' : '图片') + ''; + modal.style.display = 'flex'; + } + function closePreview() { + var v = modalBody.querySelector('video'); + if (v) { v.pause(); v.removeAttribute('src'); v.load(); } + modalBody.innerHTML = ''; + modal.style.display = 'none'; + current = null; + } + document.getElementById('previewClose').addEventListener('click', closePreview); + if (modal) modal.addEventListener('click', function (e) { + if (e.target === modal) closePreview(); + }); + document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closePreview(); }); + document.getElementById('previewAdd').addEventListener('click', function () { + if (!current) return; + post('/api/playlist/add', { path: current.path }, function () { closePreview(); reload('已加入播放列表'); }); + }); + document.getElementById('previewPlay').addEventListener('click', function () { + if (!current) return; + post('/api/playlist/play', { path: current.path }, function () { closePreview(); toast('已发送播放指令'); }); + }); + document.getElementById('previewDelete').addEventListener('click', function () { + if (!current) return; + if (!window.confirm('确定删除该文件?')) return; + post('/api/delete', { path: current.path }, function () { closePreview(); reload('已删除'); }); + }); +})(); +/* ---------- 语音 AI 测试(主动问候) ---------- */ +(function () { + var greetBtn = document.getElementById('btnVoiceGreet'); + var greetInput = document.getElementById('voiceGreetText'); + var greetHint = document.getElementById('voiceGreetHint'); + if (!greetBtn || !greetInput) return; + greetBtn.addEventListener('click', function () { + var text = (greetInput.value || '').trim(); + if (!text) { toast('请输入问候语'); return; } + greetBtn.disabled = true; + greetBtn.textContent = '发送中…'; + fetch('/api/display/command', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ action: 'voice_start', params: { text: text } }), + }) + .then(function (r) { return r.json().catch(function () { return {}; }); }) + .then(function (d) { + greetBtn.disabled = false; + greetBtn.textContent = '播报问候'; + if (d && d.ok === false) { toast('发送失败:MQTT 未发布(' + (d.error || '未知') + ')', 'error'); return; } + if (greetHint) greetHint.textContent = '✅ 已发送:展播端将进入语音对话页并主动播报「' + text + '」,请留意展播端声音。'; + toast('语音问候已发送'); + }) + .catch(function () { + greetBtn.disabled = false; + greetBtn.textContent = '播报问候'; + toast('发送失败:后端不可达', 'error'); + }); + }); +})(); diff --git a/backend/static/voice-worklets/audio-playback.js b/backend/static/voice-worklets/audio-playback.js new file mode 100644 index 0000000..469b904 --- /dev/null +++ b/backend/static/voice-worklets/audio-playback.js @@ -0,0 +1,211 @@ +// @ts-check +/** + * AudioWorkletProcessor that plays back Float32 mono samples received from + * the main thread, upsampling whatever incoming rate the server uses + * (typically 16 kHz PCM16) to the AudioContext rate (typically 48 kHz). + * + * Lifecycle / messaging: + * + * main -> worklet: + * { kind: "config", inputRate: 16000, muted: false } one-shot at startup + * { kind: "audio", samples: Float32Array } per chunk + * { kind: "clear" } wipe queue (barge-in) + * + * worklet -> main: + * { kind: "stats", queuedMs, played } every ~250 ms + * { kind: "underrun" } every time the queue + * runs dry mid-playback + * + * `muted` (default false): the Live Avatar (LiveTalking WebRTC) carries the + * same audio, so the page mutes its own playback to avoid double audio. When + * the avatar is NOT running (e.g. dev on macOS), keep muted=false so the page + * plays the backend audio itself. + * + * IMPORTANT: the read position only advances while audio is actually being + * played (`_playing`). Advancing during idle would drift `_readIdx` far past + * the next chunk's length and turn every read into NaN (silence) once audio + * arrives. + */ + +const STATS_INTERVAL_FRAMES = 12000; +const FADE_FRAMES = 32; + +class AudioPlaybackProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this._inputRate = 16000; + this._stepRatio = this._inputRate / sampleRate; + this._muted = false; + this._queue = []; + this._readIdx = 0; + this._fracPos = 0; + this._playing = false; + this._lastSample = 0; + this._framesSinceStats = 0; + this._totalPlayed = 0; + this._dbgReceived = 0; + this._dbgOutPeak = 0; + this._fadeIn = 0; + this._fadeOut = 0; + + this.port.onmessage = (e) => { + const data = e.data; + if (!data || typeof data !== "object") return; + switch (data.kind) { + case "config": + if (typeof data.inputRate === "number" && data.inputRate > 0) { + this._inputRate = data.inputRate; + this._stepRatio = this._inputRate / sampleRate; + } + if (typeof data.muted === "boolean") this._muted = data.muted; + break; + case "audio": { + // Accept Float32Array, a transferred ArrayBuffer, or a plain + // {buffer, byteOffset, length} descriptor: cross-realm structured + // cloning can deliver the samples in any of these shapes. + let arr = data.samples; + let f32 = null; + if (arr instanceof Float32Array) { + f32 = arr; + } else if (arr instanceof ArrayBuffer) { + f32 = new Float32Array(arr); + } else if (arr && typeof arr === "object" && arr.buffer instanceof ArrayBuffer) { + f32 = new Float32Array(arr.buffer, arr.byteOffset || 0, (arr.byteLength || arr.buffer.byteLength) >> 2); + } + if (f32 && f32.length > 0) { + this._queue.push(f32); + this._dbgReceived += 1; + if (!this._playing) { + this._playing = true; + this._fadeIn = FADE_FRAMES; + this._fadeOut = 0; + } + } + break; + } + case "clear": + this._queue.length = 0; + this._readIdx = 0; + this._fracPos = 0; + this._playing = false; + this._lastSample = 0; + this._fadeOut = FADE_FRAMES; + break; + } + }; + } + + _queuedSamples() { + let total = -this._readIdx; + for (const buf of this._queue) total += buf.length; + return Math.max(0, total); + } + + /** Linear-interp read at the current fractional position. */ + _readInterpolated() { + if (this._queue.length === 0) return null; + const head = this._queue[0]; + const idx = this._readIdx; + const frac = this._fracPos; + + let a = head[idx]; + let b; + if (idx + 1 < head.length) { + b = head[idx + 1]; + } else if (this._queue.length > 1) { + b = this._queue[1][0]; + } else { + b = a; + } + return a + (b - a) * frac; + } + + /** Advance the read position by `stepRatio`; pop consumed buffers. */ + _advance() { + this._fracPos += this._stepRatio; + while (this._fracPos >= 1) { + this._fracPos -= 1; + this._readIdx += 1; + } + while (this._queue.length > 0 && this._readIdx >= this._queue[0].length) { + this._readIdx -= this._queue[0].length; + this._queue.shift(); + } + } + + process(_, outputs) { + const channels = outputs[0]; + if (!channels || channels.length === 0) return true; + const out = channels[0]; + const stereo = channels.length > 1 ? channels[1] : null; + + // Muted mode (Live Avatar WebRTC carries the audio): output silence. + if (this._muted) { + for (let i = 0; i < out.length; i++) { + out[i] = 0; + if (stereo) stereo[i] = 0; + } + this._framesSinceStats += out.length; + return true; + } + + for (let i = 0; i < out.length; i++) { + let sample = 0; + + if (this._playing) { + const v = this._readInterpolated(); + if (v === null) { + // Underrun: ramp out cleanly to avoid clicks. + sample = this._lastSample * Math.max(0, 1 - 1 / FADE_FRAMES); + this._lastSample = sample; + if (Math.abs(sample) < 1e-4) { + this._playing = false; + this._lastSample = 0; + this.port.postMessage({ kind: "underrun" }); + } + } else { + sample = v; + this._lastSample = v; + this._advance(); + } + + if (this._fadeIn > 0) { + const gain = 1 - this._fadeIn / FADE_FRAMES; + sample *= gain; + this._fadeIn -= 1; + } + if (this._fadeOut > 0) { + const gain = this._fadeOut / FADE_FRAMES; + sample *= gain; + this._fadeOut -= 1; + if (this._fadeOut === 0) { + this._playing = false; + this._lastSample = 0; + } + } + + this._totalPlayed += 1; + const abs = sample < 0 ? -sample : sample; + if (abs > this._dbgOutPeak) this._dbgOutPeak = abs; + } + + out[i] = sample; + if (stereo) stereo[i] = sample; + } + + this._framesSinceStats += out.length; + if (this._framesSinceStats >= STATS_INTERVAL_FRAMES) { + this.port.postMessage({ + kind: "stats", + queuedMs: Math.round((this._queuedSamples() / this._inputRate) * 1000), + played: this._totalPlayed, + received: this._dbgReceived, + outPeak: this._dbgOutPeak, + }); + this._framesSinceStats = 0; + } + return true; + } +} + +registerProcessor("audio-playback", AudioPlaybackProcessor); diff --git a/backend/static/voice-worklets/mic-capture.js b/backend/static/voice-worklets/mic-capture.js new file mode 100644 index 0000000..a48860b --- /dev/null +++ b/backend/static/voice-worklets/mic-capture.js @@ -0,0 +1,159 @@ +// @ts-check +/** + * AudioWorkletProcessor that resamples the AudioContext rate (typically 48 kHz) + * down to 16 kHz, packs the result as little-endian Int16 PCM, and posts it + * back to the main thread in fixed-size chunks. + * + * The Hugging Face speech-to-speech WebSocket route expects the + * `input_audio_buffer.append` payload at 16 kHz PCM16 mono. + * + * Design notes: + * - 48 -> 16 is an exact 3:1 ratio so we use a 3-tap boxcar average as a + * cheap low-pass before decimating. Good enough for voice STT; we lose + * a tiny bit of >8 kHz content which the pipeline discards anyway. + * - Output frames are emitted at the cadence dictated by `chunkMs` + * (default 40 ms = 640 samples = 1280 bytes). The OpenAI Realtime + * server batches incoming audio so the cadence is flexible; 20-100 ms + * is the sweet spot. + * - Float -> Int16 saturates to [-1, 1] before scaling. + * - Optional noise gate: per-chunk RMS decides open/closed against a + * threshold; the gain ramps (fast attack, hold, slow release) so word + * onsets aren't clipped and quiet tails don't click. The gate only + * affects the audio we SEND; the main-thread visualiser taps the raw + * mic separately. We post the chunk RMS up every frame so the Settings + * mic meter can show the live level against the threshold. + */ + +const TARGET_RATE = 16000; +const DEFAULT_CHUNK_MS = 40; +// Gate envelope timing (fixed; only the threshold is user-tunable). +const GATE_ATTACK_MS = 5; // open almost instantly so word onsets survive +const GATE_HOLD_MS = 250; // stay open this long after the level drops back under +const GATE_RELEASE_MS = 80; // then fade closed over this long (no click) + +class MicCaptureProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + const chunkMs = options?.processorOptions?.chunkMs ?? DEFAULT_CHUNK_MS; + this._inputRate = sampleRate; + this._ratio = this._inputRate / TARGET_RATE; + this._chunkSamples16k = Math.round((TARGET_RATE * chunkMs) / 1000); + this._scratch = new Float32Array(0); + this._decimated = new Float32Array(this._chunkSamples16k); + this._enabled = true; + + // Noise gate state. Disabled by default (pure passthrough). + this._gateEnabled = false; + this._thresholdLin = 0; // linear amplitude; signal RMS must exceed this to open + this._gateGain = 1; // smoothed gain currently applied + this._holdRemaining = 0; // samples left before the gate may start closing + this._attackCoef = Math.exp(-1 / ((GATE_ATTACK_MS / 1000) * TARGET_RATE)); + this._releaseCoef = Math.exp(-1 / ((GATE_RELEASE_MS / 1000) * TARGET_RATE)); + this._holdSamples = Math.round((GATE_HOLD_MS / 1000) * TARGET_RATE); + + this.port.onmessage = (e) => { + const data = e.data; + if (data?.kind === "enable") this._enabled = !!data.value; + else if (data?.kind === "gate") { + this._gateEnabled = !!data.enabled; + // dB -> linear amplitude. When off, threshold 0 keeps the gate open. + this._thresholdLin = data.enabled ? Math.pow(10, data.thresholdDb / 20) : 0; + } + }; + } + + /** + * Append `incoming` to the internal scratch buffer, then emit as many + * full output chunks as we have material for. + * @param {Float32Array} incoming + */ + _ingest(incoming) { + if (incoming.length === 0) return; + const next = new Float32Array(this._scratch.length + incoming.length); + next.set(this._scratch, 0); + next.set(incoming, this._scratch.length); + this._scratch = next; + this._maybeEmit(); + } + + _maybeEmit() { + const r = this._ratio; + const n = this._chunkSamples16k; + const needIn = Math.ceil(n * r); + const dec = this._decimated; + while (this._scratch.length >= needIn) { + // 1. Decimate to 16 kHz floats and accumulate energy for the gate/meter. + let sumSq = 0; + if (Math.abs(r - 3) < 1e-6) { + // 48 kHz -> 16 kHz fast path with boxcar lowpass. + for (let i = 0; i < n; i++) { + const idx = i * 3; + const s = (this._scratch[idx] + this._scratch[idx + 1] + this._scratch[idx + 2]) / 3; + dec[i] = s; + sumSq += s * s; + } + } else { + // Generic path: linear interpolation. Slower but works at any rate + // (e.g. some Windows boxes report sampleRate=44100). + for (let i = 0; i < n; i++) { + const srcPos = i * r; + const idx = Math.floor(srcPos); + const frac = srcPos - idx; + const a = this._scratch[idx]; + const b = this._scratch[idx + 1] ?? a; + const s = a + (b - a) * frac; + dec[i] = s; + sumSq += s * s; + } + } + const rms = Math.sqrt(sumSq / n); + + // 2. Decide the gate target for this chunk, then ramp sample-by-sample. + let target = 1; + if (this._gateEnabled) { + if (rms >= this._thresholdLin) { + this._holdRemaining = this._holdSamples; // re-arm the hold + } else if (this._holdRemaining > 0) { + this._holdRemaining -= n; // coasting through the hold window + } else { + target = 0; + } + } + + // 3. Apply the (smoothed) gain and pack to Int16. + const out = new Int16Array(n); + let gain = this._gateGain; + for (let i = 0; i < n; i++) { + const coef = target > gain ? this._attackCoef : this._releaseCoef; + gain = target + (gain - target) * coef; + const s = dec[i] * gain; + const clamped = s < -1 ? -1 : s > 1 ? 1 : s; + out[i] = clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff; + } + this._gateGain = gain; + + // Shift the scratch buffer to keep only the trailing unused samples. + const consumed = Math.floor(n * r); + this._scratch = this._scratch.slice(consumed); + + // Live input level for the Settings meter (raw RMS, pre-gate). + this.port.postMessage({ kind: "level", rms }); + + if (this._enabled) { + this.port.postMessage(out.buffer, [out.buffer]); + } + // When disabled (mic muted) we silently consume input so the worklet + // stays alive and the buffer never grows unbounded. + } + } + + process(inputs) { + const input = inputs[0]; + if (!input || input.length === 0 || !input[0]) return true; + const mono = input[0]; + if (mono.length > 0) this._ingest(mono); + return true; + } +} + +registerProcessor("mic-capture", MicCaptureProcessor); diff --git a/backend/templates/admin.html b/backend/templates/admin.html new file mode 100644 index 0000000..b06f61a --- /dev/null +++ b/backend/templates/admin.html @@ -0,0 +1,460 @@ + + + + + +管理后台 · 昆明市大学生创业园 + + + +{% if not logged_in %} + +
+ +
+{% else %} + +
+
+ + +
+
+
+

昆明市大学生创业园

+
+
+ 等待大屏连接... +
+
+ + + + + + {% if from_screen %}返回展播{% endif %} + 数据大屏 + 退出 +
+
+ +
+ +
+
+
+

连接状态

+
+
+
+ + 后端服务 + 检测中... +
+
+ + MQTT Broker + 检测中... +
+
+ + 在线大屏 + 检测中... +
+
+ + 最近指令 + — +
+
+
+ + +
+
+
+

目标屏幕(按 MQTT 客户端精准控制)

+
+
+ 在线屏幕:加载中… + +
+
选择某台屏幕后,下方命令只发给它(按 client_id);「全部」则所有屏幕同步。
+
+ +
+
+
+

页面切换(控制大屏展示)

+
+
+ + + + +
+
+ + +
+
+
+

大屏控制(MQTT 指令)

+
+ + +
+
页面导航
+
+ + + + +
+
+ + +
+
企业展示墙(/wall)
+
+ + +
+
+ + + +
+
+ + +
+
人物识别(全局)
+
+ + +
+
+ + +
+
AI 助手页
+
+ + +
+
+ + + + + +
+
+ + + +
+
+ + +
+
语音对话页
+
+ + + +
+
+ + +
+
双屏控制
+
+ + +
+
在电脑另一块屏幕全屏展示(需展播机连接显示器)
+
+ + +
+
媒体轮播
+
+ + + + +
+
+ + +
+
全局通知
+
+ + +
+
+
+ + +
+
+
+

播放设置

+
+
+
+ + +
+
+ + +
+
+ + +
{{ volume }}%
+
+
+ + +
{{ sfx_volume }}%
+
+
+
+ + +
+
+
+

语音 AI 测试

+
+
+
+ + +
+
+ + +
+
+
点击后展播端将自动进入语音对话页并主动播报上述问候(LLM 生成 + TTS),用于验证语音链路。
+
+ + +
+
+
+

显示控制

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+

添加媒体

+
+
+ +
+ + +
+ +
+
+ +
+ + + +
+
+
+ + +
+
+
+

媒体库({{ files|length }})

+
+
+ {% if files %} + {% for item in files %} +
+ {% if item.type == 'image' %} + {{ item.name }} + {% else %} +
+ + ▶ +
+ {% endif %} +
+ {{ item.name }} +
+ {{ '远程' if item.source == 'url' else '本地' }} + {{ '视频' if item.type == 'video' else '图片' }} +
+
+ {{ item.url }} + +
+
+ + + +
+
+
+ {% endfor %} + {% else %} +
+
+ 暂无媒体文件
请上传或通过 URL 添加 +
+ {% endif %} +
+
+ + +
+
+
+

播放列表({{ playlist|length }})

+
+
+ {% if playlist %} + {% for item in playlist %} +
+ {% if item.type == 'image' %} + {{ item.name }} + {% else %} +
+ + ▶ +
+ {% endif %} +
+ {{ item.name }} +
+ {{ '远程' if item.source == 'url' else '本地' }} + {{ '视频' if item.type == 'video' else '图片' }} +
+
+ {{ item.url }} + +
+
+ + +
+
+
+ {% endfor %} + {% else %} +
+
▶
+ 播放列表为空
从上方媒体库添加内容 +
+ {% endif %} +
+
+
+
+ + + + + +
+
+ + + +{% endif %} + + diff --git a/backend/uv.lock b/backend/uv.lock new file mode 100644 index 0000000..a4bd56d --- /dev/null +++ b/backend/uv.lock @@ -0,0 +1,2962 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform != 'win32'", +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/8e/38aa427ed5402449e226975b649c5dc73ccadfefeb95e6aecb8f8ea4b6b6/annotated_doc-0.0.5.tar.gz", hash = "sha256:c7e58ce09192557605d8bbd92836d7e1d520ac9580096042c0bfd197efacf1bb", size = 10758, upload-time = "2026-07-28T13:50:58.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/30/e900b21425a860e195f32e37657aa1f7c7f2b1bfb26f03ca209b90933c06/annotated_doc-0.0.5-py3-none-any.whl", hash = "sha256:117bac03a25ede5df5440e855b32d556049ca169ead221505badf432fed4b101", size = 5302, upload-time = "2026-07-28T13:50:57.239Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "backend" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "certifi" }, + { name = "dashscope" }, + { name = "fastapi" }, + { name = "httpx" }, + { name = "jinja2" }, + { name = "nltk" }, + { name = "numpy" }, + { name = "openai" }, + { name = "paho-mqtt" }, + { name = "python-multipart" }, + { name = "scipy" }, + { name = "soundfile" }, + { name = "torch" }, + { name = "transformers" }, + { name = "ultralytics" }, + { name = "uvicorn", extra = ["standard"] }, + { name = "websockets" }, +] + +[package.metadata] +requires-dist = [ + { name = "certifi", specifier = ">=2024.2" }, + { name = "dashscope", specifier = ">=1.26" }, + { name = "fastapi", specifier = ">=0.110" }, + { name = "httpx", specifier = ">=0.28" }, + { name = "jinja2", specifier = ">=3.1" }, + { name = "nltk", specifier = "==3.9.4" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "openai", specifier = "==2.28.0" }, + { name = "paho-mqtt", specifier = ">=1.6,<2" }, + { name = "python-multipart", specifier = ">=0.0.9" }, + { name = "scipy", specifier = ">=1.10" }, + { name = "soundfile", specifier = ">=0.13" }, + { name = "torch", specifier = ">=2.4" }, + { name = "transformers", specifier = ">=4.40" }, + { name = "ultralytics", specifier = ">=8.4.121" }, + { name = "uvicorn", extras = ["standard"], specifier = ">=0.29" }, + { name = "websockets", specifier = ">=12" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "13.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/6d2e9047d1fb243dbaa364b01e0297534b9ed7fd27dba1c9f361519cf69b/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e32d08f71ebcdf00f0f41eab2eb37e8da94c8ed411cc9f7f7a019ce6b34abe3a", size = 6657965, upload-time = "2026-05-29T23:11:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6e/2394f8163360f8391f8f1b7e72d300a82724edb81a7b7084c799fbd4c91f/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9efb21c1ee64981e184b9e0ba5eb3179e5ba3d4b51665a6cb52b8ef3d01a7cbf", size = 5920504, upload-time = "2026-05-29T23:11:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/34/c2/ef9b6a63f7dc432712a462c816662e662e00d38caa9b861c8c2588195d03/cuda_bindings-13.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2732904099e0a4d4db774a5fc6d91ee95fae065b4d2ecabb4968c5fe2406c9d7", size = 6476660, upload-time = "2026-05-29T23:11:59.188Z" }, + { url = "https://files.pythonhosted.org/packages/b1/81/bff68ce829999c1e4209c761bbf903b1c06ec570416ddb25020864ad5907/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ab2f74ed65bfef4163ba07a8db16f1085e0729291db12a2423aff84ee8278b8", size = 6013639, upload-time = "2026-05-29T23:12:03.509Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e0/c8a1f0c8f9ffdea4f5fe6dbab89b326cef4d85caf489dad39e209da89416/cuda_bindings-13.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd4c814d311ec08c981f6dded1dbe7d4b371067ee4f6c14cccec4bde9590f80", size = 6534419, upload-time = "2026-05-29T23:12:05.633Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/83b1f563925b290f2d11a01a77a84013ba56052fe3653a5bef3ccfbb43d6/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3c772dfff49681541d59630c90f858e173ac926b9c593a2b7123f2a1043cc76", size = 5809771, upload-time = "2026-05-29T23:12:10.422Z" }, + { url = "https://files.pythonhosted.org/packages/12/20/e79b4bfe98f075195afb6343d41c498f9dbd2d161d7021d4d28bceb83581/cuda_bindings-13.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36febb7c1079d68a981dbbd8d5a67235b399802b82075c9388624719607e52b9", size = 6358584, upload-time = "2026-05-29T23:12:12.767Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/b4/d088047afe39827556df21118cac9ffd20cc3f968c99a7681494d1eb333c/cuda_pathfinder-1.6.0-py3-none-any.whl", hash = "sha256:1503af579d8379c24bdd65528379bc57039b0455be9f5f9686cf8e473a1fce51", size = 54591, upload-time = "2026-07-21T15:03:56.224Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.3.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c7/a79086a62c98befcdb8349656c6f114e2db3b8b2422f6e25c97a7f2a9a3c/cuda_toolkit-13.0.3.0-py2.py3-none-any.whl", hash = "sha256:d693caaa261214ddd7dbb60d68e71cbed884e68c2be7509778f3051da0b91c3f", size = 2512, upload-time = "2026-04-14T00:50:08.173Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +curand = [ + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusolver = [ + { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "dashscope" +version = "1.26.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohttp" }, + { name = "certifi" }, + { name = "cryptography" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "requests" }, + { name = "rich" }, + { name = "typer" }, + { name = "websocket-client" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/af/6f65d1d46f67206c80a0d24a236b0930cdffc540badba3d9d57fa4fce34b/dashscope-1.26.7-py3-none-any.whl", hash = "sha256:e7797ff99c5335589ecd0b97391611dd68dc3da6a28681dab5c522759b22922d", size = 1537542, upload-time = "2026-08-12T13:03:59.002Z" }, +] + +[[package]] +name = "distro" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, +] + +[[package]] +name = "fastapi" +version = "0.141.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/02/91e3416a8fdd715abb903a952a6bec7cdd8d14eed55d415fc8595524c319/fastapi-0.141.1.tar.gz", hash = "sha256:e8822fc40db1e1858054d7a949a888695bc9bdce70139178e33bd2871a453ca1", size = 425799, upload-time = "2026-07-29T17:18:05.568Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/03/10388a42375ee7e4ac9b94eb2c5c569c8b5795e377e701c9ac3ad63de890/fastapi-0.141.1-py3-none-any.whl", hash = "sha256:bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3", size = 131954, upload-time = "2026-07-29T17:18:04.364Z" }, +] + +[[package]] +name = "filelock" +version = "3.32.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" }, +] + +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/ef/b3c6b9b5be2f82416d73fe2ed2e96e2793cd80e7510bd6a17ca79cdd88ec/fonttools-4.63.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:37dd23e621e3b0aef1baa70a303b80aaf38449632cfc8fd2a55fb285bbccfc02", size = 2881131, upload-time = "2026-05-14T12:03:13.386Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/c815bea63117fa63e4e1c01f8a1110d2112fa003f838e6467094ec2432ce/fonttools-4.63.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a9faff9e0c1f76f9fd55899d2ce785832efebab37eb8ae13995853aef178bef0", size = 2426704, upload-time = "2026-05-14T12:03:15.801Z" }, + { url = "https://files.pythonhosted.org/packages/44/04/0b91d8e916e92ad1fac9e4624760baf0fd5ff2ead614c2f68fb21373f03f/fonttools-4.63.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef3048ef05dbb552b89817713d9cac912e00d0fde4a3105c00d29e52e10c89af", size = 5044298, upload-time = "2026-05-14T12:03:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/77/c7/2342da9830e3e9d4870305ca5d2091d2a83284f2953079b7bdd3b5e029d8/fonttools-4.63.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58dc6bb86a78d782f00f9190ca02c119cf5bbe2807536e361e18d42019f877d8", size = 4999800, upload-time = "2026-05-14T12:03:20.161Z" }, + { url = "https://files.pythonhosted.org/packages/e6/6d/67fe16c48d7ce050979b33f47e0d28a318f02da030602e944c34f7a16ef3/fonttools-4.63.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee08ebfa58f6e1aeff5697ab9582105bb620008c1caafb681e4c557e7483027b", size = 4982666, upload-time = "2026-05-14T12:03:22.87Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/3bbab338c07c71fa56269953845e92c951a61457bbbb0f1022551ea266d9/fonttools-4.63.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27fdc65af8da6f88b9c6121c47a464cbe359fcfff7ff6fc2d37a1f395d755b78", size = 5133598, upload-time = "2026-05-14T12:03:25.168Z" }, + { url = "https://files.pythonhosted.org/packages/62/f2/aa27c7f98db5b064883dadcc5283947e81e034de42e22a33675878d98b54/fonttools-4.63.0-cp312-cp312-win32.whl", hash = "sha256:af2fd1664d00a397d75f806985ddb36282091c2131a73a6485c23b4a34722263", size = 2292575, upload-time = "2026-05-14T12:03:27.496Z" }, + { url = "https://files.pythonhosted.org/packages/87/36/cccb9bc2a6ab63d1b2980374f0dca72ce95ae267c9b4cfe77455bb70d0d4/fonttools-4.63.0-cp312-cp312-win_amd64.whl", hash = "sha256:59ac449f8cca9b4ffa08d2e7bbadad87ce710d69d1eda5c3c1ce579baa987272", size = 2343211, upload-time = "2026-05-14T12:03:30.057Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/78/f34251dadb8f3921264a1d9b8946f5e542014ee2614b285261b4e40e6775/fsspec-2026.7.0.tar.gz", hash = "sha256:c803c40f4cf860b49dea58ee3e1c33cb9c790520e233537e1340049f89b82a88", size = 317040, upload-time = "2026-07-28T16:34:51.052Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584, upload-time = "2026-08-03T22:32:47.364Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381, upload-time = "2026-08-03T22:32:49.049Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982, upload-time = "2026-08-03T22:32:50.568Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445, upload-time = "2026-08-03T22:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099, upload-time = "2026-08-03T22:32:54.114Z" }, + { url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712, upload-time = "2026-08-03T22:32:55.547Z" }, + { url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906, upload-time = "2026-08-03T22:32:57.391Z" }, + { url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221, upload-time = "2026-08-03T22:32:59.123Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" }, + { url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" }, + { url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" }, + { url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" }, + { url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" }, + { url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httptools" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" }, + { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" }, + { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" }, + { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" }, + { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" }, + { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" }, + { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" }, + { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" }, + { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" }, + { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" }, + { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" }, + { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" }, + { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" }, + { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" }, + { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" }, + { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "1.27.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/9b/ddf3d02a8681f1b9ce52fda03d755dad6b74c4f8172304c4c8d2975450f9/huggingface_hub-1.27.0.tar.gz", hash = "sha256:c1fed40ea82a6b41b477f5243546549b792ae0a93abcea608cff66089bf8f8df", size = 942668, upload-time = "2026-08-07T12:48:05.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d8/95b735e183957c1f26d94c52977f09d466d55119cbbc1558ea4975e4c216/huggingface_hub-1.27.0-py3-none-any.whl", hash = "sha256:7df6827c2f956c60fbaa64646e979e566db76f619dd0a9729dfb8c5a3eb4f68d", size = 784926, upload-time = "2026-08-07T12:48:02.905Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jiter" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.11.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/64/f9a391af28f518b11ad45a8a712353c94a0aefce09d3703200e5c54b610a/matplotlib-3.11.1.tar.gz", hash = "sha256:69647db5746941c793d6e445a4cd349323ffb87d9cc958c2ad84a659b4832d30", size = 32612045, upload-time = "2026-07-18T03:39:46.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/6c/7ef7ebcb2bd9739b2b66b18b076e077f44bb46fdbe28ca0506edb3c62c79/matplotlib-3.11.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e15ef41507f3d525f46154ac9e3ae785dacde9f20e593a25de8986267892ef74", size = 9453849, upload-time = "2026-07-18T03:38:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f8/6d0c312c8d9738e7d9677f09fe5c986b3239e651a7b73a2deb38b65e4a71/matplotlib-3.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:21a67b961a6d597bca54fae826cd20695ba4a6e4d05424a08da6e13e3176fd6b", size = 9283113, upload-time = "2026-07-18T03:38:21.95Z" }, + { url = "https://files.pythonhosted.org/packages/c9/cf/b4ad2cc81b6672ea29ea04e64e350a9f9b493b0908ccd884c67eeff8f7b2/matplotlib-3.11.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba8f811b8ddfac493734d6af0b2dff96919d0c28ca0d641858dab4262777c6ea", size = 10035615, upload-time = "2026-07-18T03:38:24.315Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/4e10e033d9b66589d8ed98b84c95cdbb57033d57c1f41339d7393dbd2f2e/matplotlib-3.11.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c52f7ad20ef476806ed212380b1d54d20310c8b86bdc2c9a68b51f0024a44472", size = 10842559, upload-time = "2026-07-18T03:38:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/88/eb/799612d0f8cd3e816a10fec59329fca52cd2353264df80378dfc541ae855/matplotlib-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8b14eb22961fe865efb0e4ff167e333e428908b00115a8d800ccb65ee108e481", size = 10927532, upload-time = "2026-07-18T03:38:28.532Z" }, + { url = "https://files.pythonhosted.org/packages/88/89/56649bbaa2fd12e20f3be03dbcc135b0c8676d88bac17977599e3eb442a0/matplotlib-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:88a2a27dd9691ae448dfae4b26f59036be90c3c28757edd3553a29559d00859f", size = 9333886, upload-time = "2026-07-18T03:38:30.477Z" }, + { url = "https://files.pythonhosted.org/packages/c1/11/4d124efbbad677b7b7552f6f85a3bd432d4232f95400cea98fcd2ae36ef3/matplotlib-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:480194afceca4df2f137c2721227d3cba67121fbf4397b69cee7f83714b0a58a", size = 9007545, upload-time = "2026-07-18T03:38:32.833Z" }, + { url = "https://files.pythonhosted.org/packages/04/6c/4798363b7fb5644e309fe1fac30216e9146c9f70859d80d588c18caf5317/matplotlib-3.11.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6771b0cd7838c6a857a7209814158c0ad09bfef878db3033dd82d70ad101f191", size = 9454341, upload-time = "2026-07-18T03:38:35.001Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/6acadbe7f98df19d274bc107ac58bb439fa75df82c33dc110d71a4a8501f/matplotlib-3.11.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2abdee5ffa2fe11b2d19f7a5c63b785fb7c28cc46c7bc1814156341d9d1a33e1", size = 9283627, upload-time = "2026-07-18T03:38:37.061Z" }, + { url = "https://files.pythonhosted.org/packages/24/ea/65cec46fe241390ccea1b1754207ee28eb71c5ab866bd5f22fe47e538fa4/matplotlib-3.11.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0a19dcf73406d3746d25a5ed42d713604c9a3e024d129b102852b0d941cb9f3", size = 10035860, upload-time = "2026-07-18T03:38:39.663Z" }, + { url = "https://files.pythonhosted.org/packages/c7/10/63fdccccbabe002fb0960876baabc5e3f24d9c1bb4cfb25651457f74b3a0/matplotlib-3.11.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7389b77ed2ab0552f46d9a90b81b7b8e6dfcdc42adc36c37a0865799843e0e3e", size = 10843594, upload-time = "2026-07-18T03:38:42.144Z" }, + { url = "https://files.pythonhosted.org/packages/98/51/a1155945bff7b91381875022ac1522c5dfdac0d006be8e7df389b3134eae/matplotlib-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c90be0b73568da4f662afac580956a76e308437e641b4a45aa08925eeb67d95f", size = 10927962, upload-time = "2026-07-18T03:38:44.302Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3a/3d5e1f42dc761bf53401a62a83ff93389b37de9d2c093b2a3aa49ac34f1b/matplotlib-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:68408341f2312836fbbdf6b3c78047f65b2d8752f5fd221c3e72d348f5b34f8b", size = 9334074, upload-time = "2026-07-18T03:38:46.616Z" }, + { url = "https://files.pythonhosted.org/packages/e2/db/3f5ea5a5b64060ef5e1ff60a19170423e41ce21b8497a6fe15a36e0b43e3/matplotlib-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c1f44890d435c1b4ef52f701ad5828cb450ea97bcc83918fda6be74965d6cd2", size = 9007662, upload-time = "2026-07-18T03:38:49.112Z" }, + { url = "https://files.pythonhosted.org/packages/98/6e/c7ae5e0531425b69c0826b00ebbc264c85cab853f1cd6e096c9983c2cdc1/matplotlib-3.11.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:5e510088c27a89d53580a752f959146893563e63c330e161d159b0fee652af6f", size = 9503790, upload-time = "2026-07-18T03:38:51.527Z" }, + { url = "https://files.pythonhosted.org/packages/92/79/15be162e0a2ed546939674e2e97d0e33ec2447d86d4d4e611fa295bb178c/matplotlib-3.11.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:1524e2bdd48a93557aa47ddcfe9c225dfdd57d5a01a5c49128c20f0632980ee1", size = 9336148, upload-time = "2026-07-18T03:38:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7f/36ffe144fc4aacfe0e3ed2318f72b6755d1e73b041d619b4d393e60f5a66/matplotlib-3.11.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11664c551345553db92e61cae6cf1376f138f8c47cafdf13b64b18f3e3e9e464", size = 10049244, upload-time = "2026-07-18T03:38:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/55812d68c0a840d3a463638f48c00ab1fe338518ec49a640cb6473b444af/matplotlib-3.11.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e1f8922ba31959cf6a9dfb51be64b7f7bc582801a3957dc0c2f3afcd3537adf", size = 10860798, upload-time = "2026-07-18T03:38:58.282Z" }, + { url = "https://files.pythonhosted.org/packages/7a/64/cca444b4eb5e6c768c44fc5e1f0b5211f20ca2b282778051996e996a2bdf/matplotlib-3.11.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83235693abde86e5e0129998f80ee39fc7f58e6d56a88fafb28a9278833e9d5f", size = 10943282, upload-time = "2026-07-18T03:39:00.465Z" }, + { url = "https://files.pythonhosted.org/packages/e5/0f/a49c329d394f2e9ef38506982107e8b04ecf94dd41a9d8423ff82cc737c7/matplotlib-3.11.1-cp313-cp313t-win_amd64.whl", hash = "sha256:9a076f4fc5cdc43fdf510f5981418d25c2db4973418d9f22d8bb3dc8045ada78", size = 9383532, upload-time = "2026-07-18T03:39:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/e4/50/103e86afb806d8f64d04ede14e4cfc09dbfc25f512421ff85fdd6ebd59cf/matplotlib-3.11.1-cp313-cp313t-win_arm64.whl", hash = "sha256:216fbb93a74add02ddb4cb38ef5348f59ac00b3e84567eaf16598772d40e150a", size = 9059665, upload-time = "2026-07-18T03:39:04.607Z" }, + { url = "https://files.pythonhosted.org/packages/35/04/3079499fa8cb661ea66d13d6439d5a3ae6710a7afd5c7f72e08914f275f8/matplotlib-3.11.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:30c492d4ba9448595b6fd8708c6725963f8148e25c0d8842948da5b05f0ee8d3", size = 9456022, upload-time = "2026-07-18T03:39:07.041Z" }, + { url = "https://files.pythonhosted.org/packages/53/a2/69acfe84ec1f32930e801a5782a07fc5c79c8c6599a507b806d859d5da8e/matplotlib-3.11.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ac104be2768ffdd8655db9e71b768cbb45f2b9aa7b450cf1595e8f65d3822319", size = 9285475, upload-time = "2026-07-18T03:39:09.562Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b3/31b15a2ca56d4ddd6aaa1c884c2f51cf9a61cfaf5ca6f6fbd6343d38e6df/matplotlib-3.11.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6be943cb68bc6660ead58c55b3aa6366cba2ef7feb06460fbcce32360376f19f", size = 10847102, upload-time = "2026-07-18T03:39:11.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/0d/a17e966e620545c1548125af0b29ac812dd17b197a18a7462ac12fa859ee/matplotlib-3.11.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5af0dcda57d471440a7b5b623e70e0a61003518443d9098f211a96ecfbbc25be", size = 11131087, upload-time = "2026-07-18T03:39:13.764Z" }, + { url = "https://files.pythonhosted.org/packages/97/c5/5e100efdd67abb7de20befaa333612ef9bfc63417fb71398f904f25d083c/matplotlib-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3d3fd84082b1afbd9398466c81309e20045be20d48fe0fb18c43504d164cbbb2", size = 10929036, upload-time = "2026-07-18T03:39:16.888Z" }, + { url = "https://files.pythonhosted.org/packages/ce/04/d719a0a36930ecc8dfc801ff340f9dcfc4223f8ca5d39d06b4020032fff8/matplotlib-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:9601a1e90be21e4884c53b4f3dc3ee0544654946f9975258d691f1c2e2f119c6", size = 9489571, upload-time = "2026-07-18T03:39:19.449Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/facabdc2f1f6caba7e856db64dfedddca25f7608df07d96a1c8fd114fd3b/matplotlib-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:ae30c6109848ac0f9fa36c5d6270938487614c47ba31860bd5361266dabc5685", size = 9164486, upload-time = "2026-07-18T03:39:21.424Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/18da6cd01cf96354534f98c468a25380c68ce582a2c9dd0cae12b04af4f2/matplotlib-3.11.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:dadfe80797174e2984aae3be0b77594a3c72d2c0a40fbd4a0de48d2728caf3ae", size = 9504876, upload-time = "2026-07-18T03:39:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/b0/f0b63555a18b79d038c81fd6126f35fc4dfce0eaff48d96103348c7cf935/matplotlib-3.11.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:89b193b255f4f6f7948dbcee3691f4f341ab05d9a8874a67b45ddb4182922eda", size = 9336120, upload-time = "2026-07-18T03:39:25.797Z" }, + { url = "https://files.pythonhosted.org/packages/c6/dd/f210ec7c4a6f198d5567237048a93d0811fb5a1f1691f13320e592f95b41/matplotlib-3.11.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191163532cdefcb1571ca38a6d7e6474baccde64495783e6ba47aa07ec4b9bbb", size = 10858033, upload-time = "2026-07-18T03:39:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/d6d5324507c5fbb316db48e258c09c2807f3de03d9af47017e120070926f/matplotlib-3.11.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fdf1c818ab05d0e74002091ddaf414478a3a449ec9d51c8976d45be7e3a01e2", size = 11141827, upload-time = "2026-07-18T03:39:30.092Z" }, + { url = "https://files.pythonhosted.org/packages/0f/68/3c22e9320bdce2c4d2f1320643ef706db7a24cb7420eea28b97a2d67f5a8/matplotlib-3.11.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b937b9dba5f5f6c1e31c47abe2186c865c0914fd18f2ce0dfc39c9adcef5951d", size = 10943061, upload-time = "2026-07-18T03:39:32.356Z" }, + { url = "https://files.pythonhosted.org/packages/f6/4a/907ed190ee81a9df581e0ed5456134fc0f7cb55ffcfda2f9e54ca900761c/matplotlib-3.11.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f2912f647f3fbe1ccf085f91e213936f9101bead81a5e670565b1f1b3712f4fb", size = 9540074, upload-time = "2026-07-18T03:39:34.789Z" }, + { url = "https://files.pythonhosted.org/packages/23/d4/97c19b77e0a6e3b48581185bb65088f431cd20186076cc0f650a1757ea46/matplotlib-3.11.1-cp314-cp314t-win_arm64.whl", hash = "sha256:54d47b8ae8b579633a3902ca5b4ad6c1e132a5626d64447b2e22a66394e79987", size = 9213472, upload-time = "2026-07-18T03:39:37.141Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nltk" +version = "3.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "joblib" }, + { name = "regex" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/a1/b3b4adf15585a5bc4c357adde150c01ebeeb642173ded4d871e89468767c/nltk-3.9.4.tar.gz", hash = "sha256:ed03bc098a40481310320808b2db712d95d13ca65b27372f8a403949c8b523d0", size = 2946864, upload-time = "2026-03-24T06:13:40.641Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/91/04e965f8e717ba0ab4bdca5c112deeab11c9e750d94c4d4602f050295d39/nltk-3.9.4-py3-none-any.whl", hash = "sha256:f2fa301c3a12718ce4a0e9305c5675299da5ad9e26068218b69d692fda84828f", size = 1552087, upload-time = "2026-03-24T06:13:38.47Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, +] + +[[package]] +name = "nvidia-cublas" +version = "13.1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, + { url = "https://files.pythonhosted.org/packages/3b/cd/154ca20c38269e05eff77c1464e6c1da89f50a6390b565e9d82e06bc11e1/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:37936a16db8fe4ac1f065c2139360608a543a09275cb1a1af612e08cfa065436", size = 423138758, upload-time = "2026-04-08T18:46:58.655Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.20.0.48" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" }, + { url = "https://files.pythonhosted.org/packages/6e/5e/edb9c0ae051602c3ccaffe424256463636d639e27d7f302dde9975ef9e7a/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0c45dd8eeb50b603f07995b1b300c62ffe6a1980482b82b3bcf94a4ca9d49304", size = 366173588, upload-time = "2026-03-09T19:29:34.474Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/e1/cdc1797eadf82d3a9a575a19b33fdc871a97edbec42c00b5b5e914f4aff4/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4dca476c50bf4780d46cd0bfbd82e2bc10a08e4fef7950917ce8d7578d22a23f", size = 221051344, upload-time = "2025-09-05T18:49:51.289Z" }, + { url = "https://files.pythonhosted.org/packages/34/7d/2661f2fb3ac4302f3a246f5fc030213ac60c1fe0bce84f9783dbd831dbb7/nvidia_cusparselt_cu13-0.8.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:786ce87568c303fadb5afcc7102d454cd3040d75f6f8626f5db460d1871f4dd0", size = 170148586, upload-time = "2025-09-05T18:50:50.248Z" }, +] + +[[package]] +name = "nvidia-ml-py" +version = "13.610.43" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/b5/a8fbc356f768fa5c9cfd646668fd7d34bf55bdd1c6e20754642a64d930d4/nvidia_ml_py-13.610.43.tar.gz", hash = "sha256:65437eb73d68d0c62c931ca4d45038472faff03bd0b8729abba4b899f70d60f2", size = 52109, upload-time = "2026-06-01T18:54:08.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/45/caa600acfab94560807a20a64b5830d2cd3c3202b7f1328644d70b7d6bd8/nvidia_ml_py-13.610.43-py3-none-any.whl", hash = "sha256:f13c72698edef492f985cc225f14faafe68ae065a2e407f45bdf6f4b9b43fde8", size = 53163, upload-time = "2026-06-01T18:54:07.704Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.29.7" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/0d/daf50d44177ee0cbc7ff0a0c91eb5ff676c82be42f9a970bc7597f440c3a/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:674a12383e3c38a1bcccae7d4f3633b37852230b6047883cb2f4c2d1b36d9bf5", size = 206014712, upload-time = "2026-03-03T05:34:20.843Z" }, + { url = "https://files.pythonhosted.org/packages/67/f4/58e4e91b6919367c7aafb8e36fce9aad1a3047e536bf7e2fd560927d3a4c/nvidia_nccl_cu13-2.29.7-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:edd81538446786ec3b73972543e53bb43bcaf0bfc8ef76cb679fcc390ffe136d", size = 205976000, upload-time = "2026-03-03T05:36:24.472Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.3.33" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/ee/580ca6f29dcab0221db8706badca1bbbb084f1975c4d4e83329c3a7e31f0/nvidia_nvjitlink-13.3.33-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:26a6de7fb4c8fdaa7703d3dad720d6d427ddfea5c48a528fd97c11733ad830e5", size = 40742423, upload-time = "2026-05-26T16:54:51.613Z" }, + { url = "https://files.pythonhosted.org/packages/69/30/45414e35ff2eee7db3da037e5707037ccf9d2b5218ffbdb055ea4d5aa98a/nvidia_nvjitlink-13.3.33-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce48b37dfeb3cb1eae4cf85adacb47d7a6539ea2272870c9a3628ce275c2037e", size = 39168635, upload-time = "2026-05-26T16:54:13.906Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "openai" +version = "2.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "httpx" }, + { name = "jiter" }, + { name = "pydantic" }, + { name = "sniffio" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/87/eb0abb4ef88ddb95b3c13149384c4c288f584f3be17d6a4f63f8c3e3c226/openai-2.28.0.tar.gz", hash = "sha256:bb7fdff384d2a787fa82e8822d1dd3c02e8cf901d60f1df523b7da03cbb6d48d", size = 670334, upload-time = "2026-03-13T19:56:27.306Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/df122348638885526e53140e9c6b0d844af7312682b3bde9587eebc28b47/openai-2.28.0-py3-none-any.whl", hash = "sha256:79aa5c45dba7fef84085701c235cf13ba88485e1ef4f8dfcedc44fc2a698fc1d", size = 1141218, upload-time = "2026-03-13T19:56:25.46Z" }, +] + +[[package]] +name = "opencv-python" +version = "5.0.0.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/4c/a438d23e09ce2033c09f7b784ad2fbdb0adf529e434101ed28f142226f98/opencv_python-5.0.0.93.tar.gz", hash = "sha256:66aac3e5b5faa48d4025816592f3af19e4bfc2c68dec067bae2dbb4ca10aa9e2", size = 81802749, upload-time = "2026-07-02T06:59:53.815Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/75/76f6ade78f6102c61034f828e2a22616708df2c9504bc8d6af9dd8f73dc5/opencv_python-5.0.0.93-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:198a75138241810206a17c829dbcc40a7cb1841cda538ca86cbbfc6c7d95f898", size = 48322443, upload-time = "2026-07-02T05:50:25.466Z" }, + { url = "https://files.pythonhosted.org/packages/15/8c/bc1bda6aae69a32e9d84fc34153ba104cd25226861eb4aea33b2cea4860d/opencv_python-5.0.0.93-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:6bbc32f59e1b1a7db7b39c81f63d00625f041d333037fd8702f6da52cc39108b", size = 34782755, upload-time = "2026-07-02T05:51:30.556Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8a/b04776ec45d2dea08a1b176f1829201db3515d4ed16c35f8fcc9fa7beb16/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e2b4272e736836f66c2d176e43ab8101f3a00d45654916399f52e150c58981ac", size = 50614064, upload-time = "2026-07-02T06:53:22.604Z" }, + { url = "https://files.pythonhosted.org/packages/95/54/eb47866b94f2b5b42dde17644b78055ef1ee05aae59962c7290e55270803/opencv_python-5.0.0.93-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f8b6d0a212253dd26ad338c812f1f23ca118fdf05a9c8c6b9444f161aa8c5881", size = 71064711, upload-time = "2026-07-02T06:54:13.148Z" }, + { url = "https://files.pythonhosted.org/packages/93/da/962579f1e703cbf8c5422fd1f576467dcb3b5b0b0b81c1471c979764353a/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:08d5d91d967b58d6db86073b2ad3eaef88ca4ebdfd45c9059bf59f5ded0c7ad2", size = 49798576, upload-time = "2026-07-02T06:54:33.781Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4c/c73f828fdbcd37eaf21d08fa852544a3ca7c2dbb3ea76873d64f2ea413d1/opencv_python-5.0.0.93-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c8de2dec111122a02e8beb28e16c31904992dfd6186560b142a92c71403c1039", size = 73783032, upload-time = "2026-07-02T06:55:03.415Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4b/edaf83b996ca5a1a3d8ccad485706b9c6d4742b13b9c4586bf1c1e7d9423/opencv_python-5.0.0.93-cp37-abi3-win32.whl", hash = "sha256:4b4b1a34c79bf8d3738e3cfe9a9e67b51a79663f6b692cbdad8c31f570da4157", size = 35564734, upload-time = "2026-07-02T05:49:57.704Z" }, + { url = "https://files.pythonhosted.org/packages/21/f0/9fa6e85cb10c8eb36a0222d27e50fe381b86ce49a55446bf39f491727564/opencv_python-5.0.0.93-cp37-abi3-win_amd64.whl", hash = "sha256:f90ba04b8f73bc5c3814037699739f0156f597338a98f05956c684e7c3ca10d2", size = 44000345, upload-time = "2026-07-02T05:49:54.971Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "paho-mqtt" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/dd/4b75dcba025f8647bc9862ac17299e0d7d12d3beadbf026d8c8d74215c12/paho-mqtt-1.6.1.tar.gz", hash = "sha256:2a8291c81623aec00372b5a85558a372c747cbca8e9934dfe218638b8eefc26f", size = 99373, upload-time = "2021-10-21T10:33:59.864Z" } + +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" }, + { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" }, + { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" }, + { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" }, + { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" }, + { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" }, + { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" }, + { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, +] + +[[package]] +name = "polars" +version = "1.43.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "polars-runtime-32" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/13/3873f213304bcbaaf39e63c8b905ceb460a0524448d57f86a829f6d4d0fd/polars-1.43.2.tar.gz", hash = "sha256:c699671b99eb71ff53334d237917aaa3db5ad4dda480abcb6c80e0eaee7b677b", size = 750312, upload-time = "2026-08-01T06:28:30.872Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/fe/0888040a24e4504098b85d8ad486b14cb01cf6b030bbe479dfc2dcffc2ac/polars-1.43.2-py3-none-any.whl", hash = "sha256:22aa0cb92a1ee2d60d6a15a638b2e8e0dd99aea21ac0cd8fb29da8e382e075a9", size = 847150, upload-time = "2026-08-01T06:27:15.543Z" }, +] + +[[package]] +name = "polars-runtime-32" +version = "1.43.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/06/11b578eeef05f867e3ee31b2a2fdd8e7684c2aa47822c49935d1be789c38/polars_runtime_32-1.43.2.tar.gz", hash = "sha256:d7b7c486bccee75a6af0158b87077da3d054657e3c60036b28644f4e1c7fdbf7", size = 3095669, upload-time = "2026-08-01T06:28:32.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/fc/12e6d4ca34d820297651134cfa35f86c33e898539fc6629cbb35d0089697/polars_runtime_32-1.43.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:91abf205d4ec93f92ba95386b7f8776559ae3dfce425ed2e527efa75d117d04a", size = 53088908, upload-time = "2026-08-01T06:27:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/32/81/833b0853551deb810854f96b43dea342b6e6c9b0ea1afcccf774157d519d/polars_runtime_32-1.43.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:2cc3ff96fd44789b02eb5c15b98dfcb000101636b177d3034fae2feec19b118f", size = 47540529, upload-time = "2026-08-01T06:27:21.391Z" }, + { url = "https://files.pythonhosted.org/packages/83/55/7b2a75af14c9294d97f3bec132dd3018ddcd988bef32b5d28322150b8c11/polars_runtime_32-1.43.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10ed36e615ab362feb7406e6d084e124b445ad284caa73bd93ae7e65745ed894", size = 51366340, upload-time = "2026-08-01T06:27:24.776Z" }, + { url = "https://files.pythonhosted.org/packages/62/60/64deacb3abc70c52e2d88a808a052d1621c86a48fe9194f2c065579ab1cd/polars_runtime_32-1.43.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d5a7ae004a2723ebf4427f6d6a639f30f86af4cf077075f6b35d04711154fc3", size = 57304599, upload-time = "2026-08-01T06:27:27.875Z" }, + { url = "https://files.pythonhosted.org/packages/52/95/d6e3a236d7630e17c40d0ddee839bf2be9acf548fdc0e5ad65ed9ff0cac6/polars_runtime_32-1.43.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:09339eacc6d392206e78aabbaaa37d7276eb969b798f46cb1f367fd718798c60", size = 51520580, upload-time = "2026-08-01T06:27:30.913Z" }, + { url = "https://files.pythonhosted.org/packages/b6/5a/2deb8eac70e9a2ac26d88a66ae7cf52612865026f4f4a5e7ab11ad9d52bf/polars_runtime_32-1.43.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:452b400e59e7f56e4c6437f435e796903272a9388feee12de2bea049ae87025e", size = 55204471, upload-time = "2026-08-01T06:27:33.886Z" }, + { url = "https://files.pythonhosted.org/packages/29/9e/647401ae8a607bc0cc40ed7b8592d5b1be90ded0dc9b9d6d3aeb03f9524b/polars_runtime_32-1.43.2-cp310-abi3-win_amd64.whl", hash = "sha256:00e33c28e321410c8d66e814a90043101e3bdd9ed2c6dabda07565aa8adbbdf1", size = 52572176, upload-time = "2026-08-01T06:27:37.048Z" }, + { url = "https://files.pythonhosted.org/packages/96/8d/60a50c3f36c85218a7ffcb48c6fe2ce1f7bec799152d68b8658ebed2179c/polars_runtime_32-1.43.2-cp310-abi3-win_arm64.whl", hash = "sha256:350a4868cae85bf8b3f81b33ba47927c15256bd9264dfc8c0753f1b927eac9d3", size = 46582513, upload-time = "2026-08-01T06:27:40.025Z" }, +] + +[[package]] +name = "propcache" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "regex" +version = "2026.7.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/98/04b13f1ddfb63158025291c02e03eb42fbb7acb51d091d541050eb4e35e8/regex-2026.7.19.tar.gz", hash = "sha256:7e77b324909c1617cbb4c668677e2c6ae13f44d7c1de0d4f15f2e3c10f3315b5", size = 416440, upload-time = "2026-07-19T00:19:48.923Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/b9/d11d7e501ac8fd7d617684423ebb9561e0b998481c1e4cbc0cb212c5d74a/regex-2026.7.19-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2cc3460cedf7579948486eab03bc9ad7089df4d7281c0f47f4afe03e8d13f02d", size = 496778, upload-time = "2026-07-19T00:17:05.677Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a9/a5ab6f312f24318019170dc485d5421fe4f89e43a98640da50d95a8a7041/regex-2026.7.19-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0e9554c8785eac5cffe6300f69a91f58ba72bc88a5f8d661235ad7c6aa5b8ccd", size = 297122, upload-time = "2026-07-19T00:17:07.59Z" }, + { url = "https://files.pythonhosted.org/packages/b3/63/4cab4d7f2d384a144d420b763d97674cb70619c878ea6fcd7640d0e62143/regex-2026.7.19-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d7da47a0f248977f08e2cb659ff3c17ddc13a4d39b3a7baa0a81bf5b415430f6", size = 292009, upload-time = "2026-07-19T00:17:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/22/85/102a81b218298957d4ea7d2f084fae537a71add9d6ff93c8e67284c5f45e/regex-2026.7.19-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93db40c8de0815baab96a06e08a984bac71f989d13bab789e382158c5d426797", size = 796708, upload-time = "2026-07-19T00:17:11.542Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/dc136af5629938a037cd2b304c12240e132ec92f38be8ff9cc89af2a1f2d/regex-2026.7.19-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:66bd62c59a5427746e8c44becae1d9b99d22fb13f30f492083dfb9ad7c45cc18", size = 865651, upload-time = "2026-07-19T00:17:13.312Z" }, + { url = "https://files.pythonhosted.org/packages/e0/75/67402ae3cd9c8c988a4c805d15ee3eef015e7ca4cb112cf3e640fc1f4153/regex-2026.7.19-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1649eb39fcc9ea80c4d2f110fde2b8ab2aef3877b98f02ab9b14e961f418c511", size = 911756, upload-time = "2026-07-19T00:17:15.015Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8e/096d00c7c480ef2ff4265349b14e2261d4ab787ba1f74e2e80d1c58079c3/regex-2026.7.19-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dce8ec9695f531a1b8a6f314fd4b393adcccf2ea861db480cdf97a301d01a68", size = 801798, upload-time = "2026-07-19T00:17:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f0/41/e7ecac6edb5722417f85cc67eaf386322fbe8acf6918ec2fdc37c20dd9d0/regex-2026.7.19-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3080a7fd38ef049bd489e01c970c97dd84ff446a885b0f1f6b26d9b1ad13ce11", size = 776933, upload-time = "2026-07-19T00:17:19.347Z" }, + { url = "https://files.pythonhosted.org/packages/6f/69/03c9b3f058d66403e0ca2c938696e81d51cd4c6d47ec5265f02f96948d9a/regex-2026.7.19-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1d793a7988e04fcb1e2e135567443d82173225d657419ec09414a9b5a145b986", size = 784338, upload-time = "2026-07-19T00:17:21.057Z" }, + { url = "https://files.pythonhosted.org/packages/f6/f7/b38ab3d43f284afbb618fcd15d0e77eb786ae461ce1f6bc7494619ddc0f2/regex-2026.7.19-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e8b0abe7d870f53ca5143895fef7d1041a0c831a140d3dc2c760dd7ba25d4a8b", size = 860452, upload-time = "2026-07-19T00:17:23.119Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/ff60ef0571121714f3cf9920bc183071e384a10b556d042e0fdb06cc07a5/regex-2026.7.19-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:4e5413bd5f13d3a4e3539ca98f70f75e7fca92518dd7f117f030ebedd10b60cb", size = 765958, upload-time = "2026-07-19T00:17:24.81Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0f/bd34021162c0ab47f9a315bd56cd5642e920c8e5668a75ef6c6a6fca590d/regex-2026.7.19-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:73b133a9e6fb512858e7f065e96f1180aa46646bc74a83aea62f1d314f3dd035", size = 851765, upload-time = "2026-07-19T00:17:26.993Z" }, + { url = "https://files.pythonhosted.org/packages/2a/20/a2ca43edade0595cccfdc98636739f536d9e26898e7dbddc2b9e98898953/regex-2026.7.19-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbe6493fbd27321b1d1f2dd4f5c7e5bd4d8b1d7cab7f32fd67db3d0b2ed8248a", size = 789714, upload-time = "2026-07-19T00:17:28.699Z" }, + { url = "https://files.pythonhosted.org/packages/5d/47/e02db4015d424fc83c00ea0ac8c5e5ec14397943de9abf909d5ce3a25931/regex-2026.7.19-cp312-cp312-win32.whl", hash = "sha256:ddd67571c10869f65a5d7dde536d1e066e306cc90de57d7de4d5f34802428bb5", size = 267157, upload-time = "2026-07-19T00:17:31.051Z" }, + { url = "https://files.pythonhosted.org/packages/08/8e/c780c131f79b42ed22d1bd7da4096c2c35f813e835acd02ef0f018bd892c/regex-2026.7.19-cp312-cp312-win_amd64.whl", hash = "sha256:e30d40268a28d54ce0437031750497004c22602b8e3ab891f759b795a003b312", size = 277777, upload-time = "2026-07-19T00:17:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4c/e4d7e086449bdf379d89774bf1f89dc4a41943f3c5a6125a03905b34b5fb/regex-2026.7.19-cp312-cp312-win_arm64.whl", hash = "sha256:de9208bb427130c82a5dbfd104f92c8876fc9559278c880b3002755bbbe9c83d", size = 277136, upload-time = "2026-07-19T00:17:34.803Z" }, + { url = "https://files.pythonhosted.org/packages/5d/3d/84165e4299ff76f3a40fe1f2abf939e976f693383a08d2beea6af62bd2c1/regex-2026.7.19-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f035d9dc1d25eff9d361456572231c7d27b5ccd473ca7dc0adfce732bd006d40", size = 496552, upload-time = "2026-07-19T00:17:36.808Z" }, + { url = "https://files.pythonhosted.org/packages/02/a2/a65293e6e4cf28eb7ee1be5335a5386c40d6742e9f47fafc8fec785e16c7/regex-2026.7.19-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42572142ed0b9d5d261ba727157c426510da78e20828b66bbb855098b8a4e38", size = 296983, upload-time = "2026-07-19T00:17:38.816Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/2d0564e93d87bc48618360ddca232a2ca612bbdf53ce8465d45ca5ce14ee/regex-2026.7.19-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:40b34dd88658e4fedd2fddbf0275ac970d00614b731357f425722a3ed1983d11", size = 291832, upload-time = "2026-07-19T00:17:40.726Z" }, + { url = "https://files.pythonhosted.org/packages/07/cd/42dfbabff3dfc9603c501c0e2e2c5adbb09d127b267bf5348de0af338c15/regex-2026.7.19-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c41c63992bf1874cebb6e7f56fd7d3c007924659a604ae3d90e427d40d4fd13", size = 796775, upload-time = "2026-07-19T00:17:42.382Z" }, + { url = "https://files.pythonhosted.org/packages/df/5d/f6a4839f2b934e3eed5973fd07f5929ee97d4c98939fb275ea23c274ee16/regex-2026.7.19-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d3372064506b94dd2c67c845f2db8062e9e9ba84d04e33cb96d7d33c11fe1ae", size = 865687, upload-time = "2026-07-19T00:17:44.185Z" }, + { url = "https://files.pythonhosted.org/packages/14/b0/b47d6c36049bc59806a50bd4c86ced70bbe058d787f80281b1d7a9b0e024/regex-2026.7.19-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fce7760bf283405b2c7999cab3da4e72f7deca6396013115e3f7a955db9760da", size = 911962, upload-time = "2026-07-19T00:17:46.442Z" }, + { url = "https://files.pythonhosted.org/packages/2a/be/ff61f28f9273658cfe23acbbac5217221f6519960ed401e61dfdab12bc35/regex-2026.7.19-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0d702548d89d572b2929879bc883bb7a4c4709efafe4512cadee56c55c9bd15", size = 801817, upload-time = "2026-07-19T00:17:48.25Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bb/8b4f7f26b333f9f79e1b453613c39bb4776f51d38ae66dd0ba31d6b354ca/regex-2026.7.19-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d446c6ac40bb6e05025ccee55b84d80fe9bf8e93010ffc4bb9484f13d498835f", size = 776908, upload-time = "2026-07-19T00:17:50.183Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/610110fc5921d380516d03c26b652555f08aa0d23ea78a771231873c3638/regex-2026.7.19-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4c3501bfa814ab07b5580741f9bf78dfdfe146a04057f82df9e2402d2a975939", size = 784426, upload-time = "2026-07-19T00:17:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f5/1ef9e2a83a5947c57ebff0b377cb5727c3d5ec1992317a320d035cd0dbb6/regex-2026.7.19-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c4585c3e64b4f9e583b4d2683f18f5d5d872b3d71dcf24594b74ecc23602fa96", size = 860600, upload-time = "2026-07-19T00:17:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/a0/02/073af33a3ec149241d11c80acea91e722aa0adbf05addd50f251c4fe89c3/regex-2026.7.19-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:571fde9741eb0ccde23dd4e0c1d50fbae910e901fa7e629faf39b2dda740d220", size = 765950, upload-time = "2026-07-19T00:17:56.041Z" }, + { url = "https://files.pythonhosted.org/packages/81/a9/d1e9f819dc394a568ef370cd56cf25394e957a2235f8370f23b576e5a475/regex-2026.7.19-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:15b364b9b98d6d2fe1a85034c23a3180ff913f46caddc3895f6fd65186255ccc", size = 851794, upload-time = "2026-07-19T00:17:57.897Z" }, + { url = "https://files.pythonhosted.org/packages/03/3a/8ae83eda7579feacdf984e71fb9e70635fb6f832eeddca58427ec4fca926/regex-2026.7.19-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffd8893ccc1c2fce6e0d6ca402d716fe1b29db70c7132609a05955e31b2aa8f2", size = 789845, upload-time = "2026-07-19T00:17:59.97Z" }, + { url = "https://files.pythonhosted.org/packages/4b/23/c195cbfe5a75fdec64d8f6554fd15237b837919d2c61bdc141d7c807b08b/regex-2026.7.19-cp313-cp313-win32.whl", hash = "sha256:f0fa4fa9c3632d708742baf2282f2055c11d888a790362670a403cbf48a2c404", size = 267135, upload-time = "2026-07-19T00:18:01.958Z" }, + { url = "https://files.pythonhosted.org/packages/b2/80/a11de8404b7272b70acb45c1c05987cce60b45d5693da2e176f0e390d564/regex-2026.7.19-cp313-cp313-win_amd64.whl", hash = "sha256:d51ffd3427640fa2da6ade574ceba932f210ad095f65fcc450a2b0a0d454868e", size = 277747, upload-time = "2026-07-19T00:18:04.121Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/0f5c8eff1b4f1f3d83276d365fccecf666afcc7d947420943bf394d07adb/regex-2026.7.19-cp313-cp313-win_arm64.whl", hash = "sha256:c670fe7be5b6020b76bc6e8d2196074657e1327595bca93a389e1a76ab130ad8", size = 277129, upload-time = "2026-07-19T00:18:05.821Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4c/44b74742052cedda40f9ae469532a037112f7311a36669a891fba8984bb0/regex-2026.7.19-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db47b561c9afd884baa1f96f797c9ca369872c4b65912bc691cfa99e68340af2", size = 501134, upload-time = "2026-07-19T00:18:07.567Z" }, + { url = "https://files.pythonhosted.org/packages/f0/45/bbd038b5e39ee5613a5a689290145b40058cc152c41de9cc23639d2b9734/regex-2026.7.19-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:65dcd28d3eba2ab7c2fd906485cc301392b47cc2234790d27d4e4814e02cdfda", size = 299418, upload-time = "2026-07-19T00:18:09.38Z" }, + { url = "https://files.pythonhosted.org/packages/65/38/c5bde94b4cedfd5850d64c3f08222d8e1600e84f6ee71d9b44b4b8163f74/regex-2026.7.19-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f2e7f8e2ab6c2922be02c7ec45185aa5bd771e2e57b95455ee343a44d8130dff", size = 294486, upload-time = "2026-07-19T00:18:11.188Z" }, + { url = "https://files.pythonhosted.org/packages/d7/6a/2f5e107cb26c960b781967178899daf2787a7ab151844ed3c01d6fc95474/regex-2026.7.19-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe31f28c94402043161876a258a9c6f757cb485905c7614ce8d6cd40e6b7bdc1", size = 811643, upload-time = "2026-07-19T00:18:12.975Z" }, + { url = "https://files.pythonhosted.org/packages/37/d4/a2f963406d7d73a62eed84ba05a258afb6cad1b21aa4517443ce40506b78/regex-2026.7.19-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f8f6fa298bb4f7f58a33334406218ba74716e68feddf5e4e54cd5d8082705abf", size = 871081, upload-time = "2026-07-19T00:18:14.733Z" }, + { url = "https://files.pythonhosted.org/packages/45/a3/44be546340bedb15f13063f5e7fe16793ea4d9ea2e805d09bd174ac27724/regex-2026.7.19-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cc1b2440423a851fad781309dd87843868f4f66a6bcd1ddb9225cf4ec2c84732", size = 917372, upload-time = "2026-07-19T00:18:16.724Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f6/e0870b0fd2a40dba0074e4b76e514b21313d37946c9248453e34ec43923e/regex-2026.7.19-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ac59a0900474a52b7c04af8196affc22bd9842acb0950df12f7b813e983609a", size = 816089, upload-time = "2026-07-19T00:18:18.617Z" }, + { url = "https://files.pythonhosted.org/packages/ae/27/957e8e22690ad6634572b39b71f130a6105f4d0718bb16849eac00fff147/regex-2026.7.19-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4896db1f4ce0576765b8272aa922df324e0f5b9bb2c3d03044ff32a7234a9aba", size = 785206, upload-time = "2026-07-19T00:18:20.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/a4/186e410941e731037c01166069ab86da9f65e8f8110c18009ccf4bd623ee/regex-2026.7.19-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4e6883a021db30511d9fb8cfb0f222ce1f2c369f7d4d8b0448f449a93ba0bdfc", size = 800431, upload-time = "2026-07-19T00:18:22.716Z" }, + { url = "https://files.pythonhosted.org/packages/73/9f/e4e10e023d291d64a33e246610b724493bf1ce98e0e59c9b7c837e5acfb7/regex-2026.7.19-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:09523a592938aa9f587fb74467c63ff0cf88fc3df14c82ab0f0517dcf76aaa62", size = 864906, upload-time = "2026-07-19T00:18:24.772Z" }, + { url = "https://files.pythonhosted.org/packages/24/57/ccb20b6be5f1f52a053d1ba2a8f7a077edb9d918248b8490d7506c6832b3/regex-2026.7.19-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1ebac3474b8589fce2f9b225b650afd61448f7c73a5d0255a10cc6366471aed1", size = 773559, upload-time = "2026-07-19T00:18:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/a3/82/f3b263cf8fad927dc102891da8502e718b7ff9d19af7a2a07c03865d7188/regex-2026.7.19-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4a0530bb1b8c1c985e7e2122e2b4d3aedd8a3c21c6bfddae6767c4405668b56e", size = 857739, upload-time = "2026-07-19T00:18:29.107Z" }, + { url = "https://files.pythonhosted.org/packages/47/2e/1687bd1b6c2aed5e672ccf845fc11557821fe7366d921b50889ea5ce57bf/regex-2026.7.19-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef7eeb108c47ce7bcc9513e51bcb1bf57e8f483d52fce68a8642e3527141ae0", size = 804522, upload-time = "2026-07-19T00:18:31.362Z" }, + { url = "https://files.pythonhosted.org/packages/76/7c/cc4e7655181b2d9235b704f2c5e19d8eff002bbc437bae59baee0e381aca/regex-2026.7.19-cp313-cp313t-win32.whl", hash = "sha256:64b6ca7391a1395c2638dd5c7456d67bea44fc6c5e8e92c5dc8aa6a8f23292b4", size = 269141, upload-time = "2026-07-19T00:18:33.479Z" }, + { url = "https://files.pythonhosted.org/packages/bb/14/961b4c7b05a2391c32dbc85e27773076671ef8f97f36cec70fe414734c02/regex-2026.7.19-cp313-cp313t-win_amd64.whl", hash = "sha256:f04b9f56b0e0614c0126be12c2c2d9f8850c1e57af302bd0a63bed379d4af974", size = 280036, upload-time = "2026-07-19T00:18:35.419Z" }, + { url = "https://files.pythonhosted.org/packages/ce/67/795644550d788ddbb6dc458c95895f8009978ea6d6ea76b005eb3f45e8c9/regex-2026.7.19-cp313-cp313t-win_arm64.whl", hash = "sha256:fcee38cd8e5089d6d4f048ba1233b3ad76e5954f545382180889112ff5cb712d", size = 279394, upload-time = "2026-07-19T00:18:37.454Z" }, + { url = "https://files.pythonhosted.org/packages/d2/25/0c4c452f8ef3efe456745b2f33195f5904b573fb4c2ff3f0cb9ec188461e/regex-2026.7.19-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a81758ed242b861b72e778ba34d41366441a2e10b16b472784c88da2dea7e2dd", size = 496750, upload-time = "2026-07-19T00:18:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/24/9e/b70ca6c1704f6c7cd32a9e143c86cc5968d10981eca284bad670c245ea7d/regex-2026.7.19-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4aa5435cdb3eb6f55fe98a171b05e3fbcd95fadaa4aa32acf62afd9b0cfdbcac", size = 297093, upload-time = "2026-07-19T00:18:41.583Z" }, + { url = "https://files.pythonhosted.org/packages/87/74/0b692da2520d51fbff19c88b83d97e4c702909dd02386c585998b7e2dbed/regex-2026.7.19-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:60be8693a1dadc210bbcbc0db3e26da5f7d01d1d5a3da594e99b4fa42df404f5", size = 292043, upload-time = "2026-07-19T00:18:43.347Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a7/1d478e614016045a33feae57446215f9fd65b665a5ceb2f891fb3183bc52/regex-2026.7.19-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19662dbedbe783d323196312d38f5ba53cf56296378252171985da6899887d3", size = 797214, upload-time = "2026-07-19T00:18:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ae/11b9c9411d92c30e3d2db32df5a31133e4a99a8fc397a604fd08f6c4bffb/regex-2026.7.19-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d15df07081d91b76ff20d43f94592ee110330152d617b730fdbe5ef9fb680053", size = 866433, upload-time = "2026-07-19T00:18:47.315Z" }, + { url = "https://files.pythonhosted.org/packages/b1/62/2b2efc4992f91d6d204b24c647c9f9412e85379d92b7c0ab9fdae622327e/regex-2026.7.19-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:56ad4d9f77df871a99e25c37091052a02528ec0eb059de928ee33956b854b45b", size = 911360, upload-time = "2026-07-19T00:18:49.588Z" }, + { url = "https://files.pythonhosted.org/packages/14/71/986ceea9aa3da548bf1357cad89b63915ec6d21ec957c8113b29ece567df/regex-2026.7.19-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7322ec6cc9fba9d49ab888bb82d67ac5625627aa168f0165139b17018df3fb8a", size = 801275, upload-time = "2026-07-19T00:18:51.767Z" }, + { url = "https://files.pythonhosted.org/packages/15/be/ce9d9534b2cda96eab32c548261224b9b4e220a4126f098f60f42ae7b4cd/regex-2026.7.19-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9c7472192ebfad53a6be7c4a8bfb2d64b81c0e93a1fc8c57e1dd0b638297b5d1", size = 777131, upload-time = "2026-07-19T00:18:54.053Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/58b5c710f2c3929515a25f3a1ca0dad0dcd4518d4fff3cf23bc7adb8dcd2/regex-2026.7.19-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c10b82c2634df08dfb13b1f04e38fe310d086ee092f4f69c0c8da234251e556e", size = 785020, upload-time = "2026-07-19T00:18:56.579Z" }, + { url = "https://files.pythonhosted.org/packages/84/03/5fe091935b74f15fe0f97998c215cae418d1c0413f6258c7d4d2e83aa37f/regex-2026.7.19-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:17ed5692f6acc4183e98331101a5f9e4f64d72fe58b753da4d444a2c77d05b12", size = 861263, upload-time = "2026-07-19T00:18:58.64Z" }, + { url = "https://files.pythonhosted.org/packages/d8/fa/d60bf82e10841eef62a9e32aac401468f05fddfbcb2942e342b1ba3d2433/regex-2026.7.19-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:22a992de9a0d91bda927bf02b94351d737a0302905432c88a53de7c4b9ce62e2", size = 766199, upload-time = "2026-07-19T00:19:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5d/11e64d151b0662b81d6bf644c74dc118d461df85bdf2577fadbbf751788a/regex-2026.7.19-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:618a0aed532be87294c4477b0481f3aa0f1520f4014a4374dd4cf789b4cd2c97", size = 851317, upload-time = "2026-07-19T00:19:03.015Z" }, + { url = "https://files.pythonhosted.org/packages/7c/34/532efb87488d90807bae6a443d357ee5e2728a478c597619c8aaa17cc0bd/regex-2026.7.19-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2ce9e679f776649746729b6c86382da519ef649c8e34cc41df0d2e5e0f6c36d4", size = 789557, upload-time = "2026-07-19T00:19:05.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/90/3a8d5ca977171ec3ae21a71207d2228b2663bde14d7f7ef0e6363ecf9290/regex-2026.7.19-cp314-cp314-win32.whl", hash = "sha256:73f272fba87b8ccfe70a137d02a54af386f6d27aa509fbffdd978f5947aae1aa", size = 272531, upload-time = "2026-07-19T00:19:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/96/e1/8862885e70409de70e8c005f57fb2e7be8d9ef0317250d60f4c9660a300d/regex-2026.7.19-cp314-cp314-win_amd64.whl", hash = "sha256:d721e53758b2cca74990185eb0671dd466d7a388a1a45d0c6f4c13cef41a68ac", size = 280831, upload-time = "2026-07-19T00:19:09.46Z" }, + { url = "https://files.pythonhosted.org/packages/08/82/2693e53e29f9104d9de95d37ce4dd826bd32d5f9c0085d3aa6ac042675c4/regex-2026.7.19-cp314-cp314-win_arm64.whl", hash = "sha256:65fa6cb38ed5e9c3637e68e544f598b39c3b86b808ed0627a67b68320384b459", size = 281099, upload-time = "2026-07-19T00:19:11.398Z" }, + { url = "https://files.pythonhosted.org/packages/92/b7/9a01aa16461a18cde9d7b9c3ab21e501db2ce33725f53014342b91df2b0a/regex-2026.7.19-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:5a2721c8720e2cb3c209925dfb9200199b4b07361c9e01d321719404b21458b3", size = 501121, upload-time = "2026-07-19T00:19:13.425Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/bbaeca815dc9191c424c94a4fdc5c87c75748a64a6271821212ebdd4e1a3/regex-2026.7.19-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:199535629f25caf89698039af3d1ad5fcae7f933e2112c73f1cdf49165c99518", size = 299415, upload-time = "2026-07-19T00:19:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d6/0dd1a321afaab95eb7ff44aa0f637301786f1dc71c6b797b9ed236ed8890/regex-2026.7.19-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9b60d7814174f059e5de4ab98271cc5ba9259cfea55273a81544dceea32dc8d9", size = 294483, upload-time = "2026-07-19T00:19:17.879Z" }, + { url = "https://files.pythonhosted.org/packages/92/5f/40bacf91d0904f812e13bbbab3864604c463eced8afdc54aeaa50492ea95/regex-2026.7.19-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dbece16025afda5e3031af0c4059207e61dcf73ef13af844964f57f387d1c435", size = 811833, upload-time = "2026-07-19T00:19:20.102Z" }, + { url = "https://files.pythonhosted.org/packages/94/7c/4902744261f775aeede8b5627314b38482da29cf49a57b66a6fb753246c5/regex-2026.7.19-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d24ecb4f5e009ea0bd275ee37ad9953b32005e2e5e60f8bbae16da0dbbf0d3a0", size = 871270, upload-time = "2026-07-19T00:19:22.365Z" }, + { url = "https://files.pythonhosted.org/packages/16/70/6980c9be6bf21c0a60ed3e0aea39cf419ecf3b08d1d9947bc56e196ef186/regex-2026.7.19-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8cae6fd77a5b72dae505084b1a2ee0360139faf72fedbab667cd7cc65aae7a6a", size = 917534, upload-time = "2026-07-19T00:19:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/52/92/8b2bd872782ce8c42691e39acb38eb8efe014e5ddb78ad7d943d6f197ce9/regex-2026.7.19-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9724e6cb5e478cd7d8cabf027826178739cb18cf0e117d0e32814d479fa02276", size = 816135, upload-time = "2026-07-19T00:19:26.919Z" }, + { url = "https://files.pythonhosted.org/packages/de/2d/33a602f657bdc4041f17d79f92ab18261d255d91a06117a6e29df023e5e2/regex-2026.7.19-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:572fc57b0009c735ee56c175ea021b637a15551a312f56734277f923d6fd0f6c", size = 785492, upload-time = "2026-07-19T00:19:29.192Z" }, + { url = "https://files.pythonhosted.org/packages/9e/36/0987cf4cb271680064a70d24a475873775a151d0b7058698a006cb0cae4a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:20568e182eb82d39a6bf7cff3fd58566f14c75c6f74b2c8c96537eecf9010e3a", size = 800658, upload-time = "2026-07-19T00:19:31.392Z" }, + { url = "https://files.pythonhosted.org/packages/a8/24/c14f31c135e1ba55fa4f9a58ca98d0842512bf6188230763c31c8f449e3b/regex-2026.7.19-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:1d58561843f0ff7dc78b4c28b5e2dc388f3eff94ebc8a232a3adba961fc00009", size = 865073, upload-time = "2026-07-19T00:19:33.485Z" }, + { url = "https://files.pythonhosted.org/packages/14/85/181a12211f22469f24d2de1ebddfe397d2396e2c29013b9a58134a91069a/regex-2026.7.19-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:61bb1bd45520aacd56dd80943bd34991fb5350afdd1f36f2282230fd5154a218", size = 773684, upload-time = "2026-07-19T00:19:35.599Z" }, + { url = "https://files.pythonhosted.org/packages/23/58/bd1a0c1a62251366f8d21f41b1ea3c76994962071b8b6ea42f72d505c0f0/regex-2026.7.19-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:cd3584591ea4429026cdb931b054342c2bcf189b44ff367f8d5c15bc092a2966", size = 857769, upload-time = "2026-07-19T00:19:37.738Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/f7e2dad6756b2fe1fe75dd90a628c3b45f249d39f948dd90cd2476325417/regex-2026.7.19-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5cc26a66e212fa5d6c6170c3a40d99d888db3020c6fdab1523250d4341382e44", size = 804546, upload-time = "2026-07-19T00:19:40.229Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d7/01d31d5bdb09bc026fab77f59a371fdf8f9b292e4810546c56182ca70498/regex-2026.7.19-cp314-cp314t-win32.whl", hash = "sha256:2c4e61e2e1be56f63ec3cc618aa9e0de81ef6f43d177205451840022e24f5b78", size = 274526, upload-time = "2026-07-19T00:19:42.398Z" }, + { url = "https://files.pythonhosted.org/packages/52/0e/cea4ce73bc0a8247a0748228ae6669984c7e1f8134b6fa66e59c0572e0ea/regex-2026.7.19-cp314-cp314t-win_amd64.whl", hash = "sha256:c639ea314df70a7b2811e8020448c75af8c9445f5a60f8a4ced81c306a9380c2", size = 283763, upload-time = "2026-07-19T00:19:44.644Z" }, + { url = "https://files.pythonhosted.org/packages/6f/b6/26e41975febae63b7a6e3e02f32cff6cff2e4f10d19c929082f56aebf7c6/regex-2026.7.19-cp314-cp314t-win_arm64.whl", hash = "sha256:9a15e785f244f3e07847b984ce8773fc3da10a9f3c131cc49a4c5b4d672b4547", size = 283451, upload-time = "2026-07-19T00:19:46.639Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + +[[package]] +name = "setuptools" +version = "84.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soundfile" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, + { name = "numpy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/db/949331952a6fb1c5b12e9de80fd08747966c2039d1a61db4764fbd3981c2/soundfile-0.14.0.tar.gz", hash = "sha256:ba1c1a2d618bca5c406647c83b89f07cc8810fa506a50622a6993ba130c1de11", size = 47842, upload-time = "2026-06-06T08:58:47.869Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/d1/5e338af9ca6ed0786cd5bb03f6d60de1c325728c1189014f3b59aae7403c/soundfile-0.14.0-py2.py3-none-any.whl", hash = "sha256:8ba81ae3a89fd5ab3bef8a8eb481fbbe794e806309675a89b4df48b8d31908a8", size = 26799, upload-time = "2026-06-06T08:58:33.269Z" }, + { url = "https://files.pythonhosted.org/packages/7e/72/c6b21e58d3113596e7e8de0a08d6f1d95173492cfbca0a4db14148cbba2a/soundfile-0.14.0-py2.py3-none-macosx_10_9_x86_64.whl", hash = "sha256:19be05428da76ed61a4cad29b8e4bcf43a3e5c100089d2ec81dc961eed1b0dd4", size = 1144568, upload-time = "2026-06-06T08:58:35.231Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/dfdd6f8c748988427119f75eb860a3cedd858d1aea1fe28f39ad8559ef22/soundfile-0.14.0-py2.py3-none-macosx_11_0_arm64.whl", hash = "sha256:d828d35a059626da52f1415b5faee610aeab393319cb3fc4a9aef47b619fc14c", size = 1103726, upload-time = "2026-06-06T08:58:37.948Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f8/fc39fad6f879633461d27394cd1ddaf1f769ffa0597dca35872f51b16461/soundfile-0.14.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:e85724a90bc99a6e8062c0b4ddf725f53b2a3b70afd4da875e9d2cfc4e92f377", size = 1238050, upload-time = "2026-06-06T08:58:39.932Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a2/70fd4432b924684c372df8b0a45708c36c057ef3596c9eb53e0a806b980b/soundfile-0.14.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:1e38bac1853412871318e82a1ba69a8be677619b56025bbfcccdb41b6cafe82d", size = 1315963, upload-time = "2026-06-06T08:58:41.716Z" }, + { url = "https://files.pythonhosted.org/packages/d9/34/c9e80783d83eab739a9531fdee03675d53e0bf1b2ccb4bb3af5844675046/soundfile-0.14.0-py2.py3-none-win32.whl", hash = "sha256:0a6ae43c50c71b4e020cc55382925cb89451c1ed1a0c3d0f5d802da269226849", size = 902199, upload-time = "2026-06-06T08:58:43.289Z" }, + { url = "https://files.pythonhosted.org/packages/ed/97/b39c18ac1df45e755ca22b8b00e872929da5d107998a207a5e4ac831bfda/soundfile-0.14.0-py2.py3-none-win_amd64.whl", hash = "sha256:299491d3499460fb1b74bb4bd78b57ffc2d243a5fafa7b6ec1b264875c78453e", size = 1021480, upload-time = "2026-06-06T08:58:45.016Z" }, + { url = "https://files.pythonhosted.org/packages/f4/83/55c65e61cf457805ce2ec157c1c6ae17715d0851aa2374422de0538838ca/soundfile-0.14.0-py2.py3-none-win_arm64.whl", hash = "sha256:e090704718e124e7c844695236f1fce8d18a5e761eaf7c82dfcd124620805f98", size = 888858, upload-time = "2026-06-06T08:58:46.593Z" }, +] + +[[package]] +name = "starlette" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b5/b4/205b0d5241d934e8add0c38aa924c4f9fb7330834ff11e5444db964ec3f9/starlette-1.6.0.tar.gz", hash = "sha256:d4e3ac5e546444960c710297a3c9fc3f7ebae1b7e963f3d36173b49da535be9b", size = 2716969, upload-time = "2026-08-08T18:27:57.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/cb/6a6a47d5b464bd08695d254f3da6e7986cc70c9fa5d778eda57538edfe56/starlette-1.6.0-py3-none-any.whl", hash = "sha256:a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c", size = 75969, upload-time = "2026-08-08T18:27:56.196Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "torch" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "python_full_version < '3.15' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/3a/ed0f4d4d1dcde03bced7aac9a28e800abcdc0cbd06b6775044c9fbd877b7/torch-2.13.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2fe228aba290d14b9f31b049be550dbd469c3fd3013d7a19705b30454da97027", size = 111213045, upload-time = "2026-07-08T16:05:22.997Z" }, + { url = "https://files.pythonhosted.org/packages/df/a9/f6a2a4d763ff1df02e9a64c477029db614295bc9367f4131223791ccc243/torch-2.13.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:572df8be8ffb4599c88cbd6a0726f1f854f4da65d2e3c09f0e2c2283333cd6d4", size = 427210998, upload-time = "2026-07-08T16:04:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/82/fea946351658e6534db52d2cc12bc53087cbf87f9440c5f180f367c1950b/torch-2.13.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:796633c4cdf0fe2cdced72d8f88f22e73dbcfce83132763162f6d4bff13b820b", size = 526605292, upload-time = "2026-07-08T16:04:22.81Z" }, + { url = "https://files.pythonhosted.org/packages/21/d6/e8f3c6f7e01f626f77259de9860d2a78bc84c40539e28e79b7e98b0bb659/torch-2.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:024c6cc0c1b085f2f91f20a3dc27b0471d021c31ce84b81be3afdc39f791fd9d", size = 122057313, upload-time = "2026-07-08T16:03:53.43Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fa/c1c10b7aff4a9a3e8956d4f0a5f468fa6db7abc3208805719076772b4833/torch-2.13.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:33449899ce5496c1b84b4853179d94fd102028ae1407314d9fb956bb79e70d09", size = 111213743, upload-time = "2026-07-08T16:03:28.579Z" }, + { url = "https://files.pythonhosted.org/packages/11/18/9ecb37b56293a0be8d80f810bf672a72fe7e02f8b475d5ef1b9bf8a0d748/torch-2.13.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:1e09d6a722504957c694faceca843acde562786df1144ebcc5a74075ec7f6005", size = 427213008, upload-time = "2026-07-08T16:03:44.106Z" }, + { url = "https://files.pythonhosted.org/packages/d4/5a/7c50ba1b7b713d71d34669c6d13dab0a11531a3eceb0307a5162dbfec0f7/torch-2.13.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:a3a9a21312872af8a26950b2c15680335a386a1f56ed03e780653d78b9607e9e", size = 526602329, upload-time = "2026-07-08T16:03:12.649Z" }, + { url = "https://files.pythonhosted.org/packages/91/3d/e7adcc6aaf36961cd18f56cf8ad0f3058c3a5c84ccf391762176c94581b8/torch-2.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:49b58f1e2c52440abb6f17c28f0335fe6c6d01ad1a7f55b0183b81e4b34d64e6", size = 122057920, upload-time = "2026-07-08T16:03:01.808Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/6dcc7f0c07052102dd36f83cbc5800842a909c8c3fbf1a7f8a5844954de9/torch-2.13.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d849b390e07d8d333ce8ecaf91b273c656c598379a19c9acf1318a883f6b391c", size = 111227066, upload-time = "2026-07-08T16:03:33.6Z" }, + { url = "https://files.pythonhosted.org/packages/e9/09/2c10e8cd0e00fa5d23c052df6ce467eaa7182399f5e0f824f1e4ff42ccae/torch-2.13.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:a3893dc2da0a972a8ca5d698c85a9f967559ac5f8ee1797b77408aa8734d073c", size = 427226309, upload-time = "2026-07-08T16:02:53.127Z" }, + { url = "https://files.pythonhosted.org/packages/76/c6/22c2102bbef14ca6a6cb4c20e42f088e49c5f812be4e160ae57502e325f9/torch-2.13.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:49f1ea385c754e54919408a9bb3b5a72b0b755bbe2c916c1d6f70afbec4908a2", size = 526614507, upload-time = "2026-07-08T16:02:16.441Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0c/7d1deb6bce5bc3e6042caf39100ac768eba3b9a098e1dddd16f75bd6489b/torch-2.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f8573e3ce9ebcd53fe922f01077a6085ccdfbe5f12fd215883a9d87d7a744fd", size = 122051871, upload-time = "2026-07-08T16:03:23.521Z" }, + { url = "https://files.pythonhosted.org/packages/f4/ce/aa8b7f9949d32e0f2f624f342bc3b48112c1b8a130288465938bc83bcbf9/torch-2.13.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c28def70706c2f9ecc752574766e8ae4da9b810ab6676b611166761a78a9f1e1", size = 111537025, upload-time = "2026-07-08T16:02:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/69/d1/491e3a0389430946145888b0203f2b6a759ce2a61481b96a85c2da4f2ced/torch-2.13.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:31061ff56ed8fbf26c749806905aeb749ebeb819810fd5d52508aa5afd90dddc", size = 427219769, upload-time = "2026-07-08T16:02:31.18Z" }, + { url = "https://files.pythonhosted.org/packages/9a/1d/38006e045bf0a1fc28ef01e757c554e59e59a8770c284bc4f47b14e60441/torch-2.13.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cc26eead4cf51d0b544e31e364dcf000846549c273bd148936fe9d24d29acb92", size = 526571320, upload-time = "2026-07-08T16:01:59.348Z" }, + { url = "https://files.pythonhosted.org/packages/56/94/655c91992a882bd5071aa0b5d22a07dbb130d801e872be97c0b627a7c693/torch-2.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:a7de8a313090dc5c7d7ba4bfe5c3be222528f9a4dba1acc83bddb1157360c4b8", size = 122306773, upload-time = "2026-07-08T16:02:39.832Z" }, +] + +[[package]] +name = "torchvision" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/15/49/c1cab1ecbb3ff1a380a3f99283db1dee61b8afe354f6352c643b65937130/torchvision-0.28.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:e9f54c30cd52e3ef7fd034cc69b7bb7e0964e1c8f8743e018ab92e95b40f9eee", size = 1856020, upload-time = "2026-07-08T16:07:52.182Z" }, + { url = "https://files.pythonhosted.org/packages/f0/4c/95233776e2def960e5abb7a07931230a545f43717a56a1e1140162033598/torchvision-0.28.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5cf78ebc401ce64ae19b8c55de866bb836797d559a4de9c25ccbe74cfa642d3a", size = 7842127, upload-time = "2026-07-08T16:07:53.446Z" }, + { url = "https://files.pythonhosted.org/packages/93/e4/e9b2495d0d57b9f60d63c57d0a910410a81b4b073bf70917bef815291119/torchvision-0.28.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:028a3d481b37d785605620d7cdad897064c5a55bae2aa1f2658766333e291940", size = 7675040, upload-time = "2026-07-08T16:07:58.017Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9c/55ed9cb6dfe3ee9c837df5cd0e758372e5829aa38b8dd71343aa632cc4e2/torchvision-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:87dc16b2df427c1318ad335f1e2be2b3b15b2cf20f7934c83b0505a48425ee5d", size = 4085785, upload-time = "2026-07-08T16:07:50.928Z" }, + { url = "https://files.pythonhosted.org/packages/20/55/08a726c14c67b37c8aca04b077766909f1c7ed23f76116884fe63b9bd033/torchvision-0.28.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:d483b4aa3f5237569053f749cd1a2b5bb548ca456e40461a5dd087f21149d123", size = 1856021, upload-time = "2026-07-08T16:07:45.386Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/40beacd53809194f5259e590d1afaeaa8ad57da15f77c646e6560bcc4616/torchvision-0.28.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:bb6dd6918460ed89cc7644adcc2402991474d6933cf1ce92b390641cb233fddf", size = 7797014, upload-time = "2026-07-08T16:07:43.04Z" }, + { url = "https://files.pythonhosted.org/packages/32/db/062cdb5a84380a60439775311fff34d89229760d2a50680393dc18699956/torchvision-0.28.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad7b3a439265cc3739a4ab5b4c998c0e38ea99c0ee7ca4dea35c5d0b099ec237", size = 7674669, upload-time = "2026-07-08T16:07:38.91Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a6/b4081e2d04e1541abf82785ac9e5178a494c19330391f551356c8c18b7b3/torchvision-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:7e9dd6f60d6e15f8dc27d4f877fdb6002fc70d70272412135f1c2ff9cfa08d3b", size = 4157380, upload-time = "2026-07-08T16:07:40.22Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b9/da40eca5bbe9596c12ae9899ab7abaf887f5e20f29d08b924b4633714821/torchvision-0.28.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3bd9dba55224a9db4a2d77f6feaa5651770d8c8e86d3d0ddb0fa6bec54c8712b", size = 1856014, upload-time = "2026-07-08T16:07:44.282Z" }, + { url = "https://files.pythonhosted.org/packages/06/d6/313aafd3df4eaf5f330211bd4e75b7598bddbfee4f55580d3b58536e1b20/torchvision-0.28.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:89f90e29b0966352811b12589f3a3c61943bf2bb9487b9d7bbec10efb1096bb5", size = 7796873, upload-time = "2026-07-08T16:07:30.907Z" }, + { url = "https://files.pythonhosted.org/packages/b3/41/31f8e959ab8f942600b6357f8999c21d779d5fd3304b0fd204ff4b518239/torchvision-0.28.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:36beb0782976906069ca03d4c9aacaf4b6b838b06ed6c20960ea9c51cce7acdd", size = 7674634, upload-time = "2026-07-08T16:07:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/15/15/4c5115253fd470672cdac0a1cf139e06b4f3e29d041238a2b255937f63be/torchvision-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:3557cc7b539f46dabcda2b6f2b14017ccbeef024de466d4fc5835fc3f287f769", size = 4184005, upload-time = "2026-07-08T16:07:35.805Z" }, + { url = "https://files.pythonhosted.org/packages/6a/80/822a6163da716f8a78141cf6678d74e26a572285d4ea866ef8aa657bb307/torchvision-0.28.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:09ce8f56e81f19b9c378ae7bb109f83f6659fd8bc3cd14241a48e4af46e9ed49", size = 1856011, upload-time = "2026-07-08T16:07:33.404Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d1/cd3f9463b39a790ec8c0c2f6e6c8061edb1562114d04fcdfa786ed889345/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:62c7d110f86a039245b587e4fae60278c649f3bd42ff79cfbc1178eca4e72542", size = 7796742, upload-time = "2026-07-08T16:07:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/3e0a7ad18e99831e2d7f4713d3be717b7159ff5a920862dd5c23c454aa71/torchvision-0.28.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:904cf89af220f8c6b2ed0296bb5065b474ce43b77558e48b2bf9de8b0ba17204", size = 7675526, upload-time = "2026-07-08T16:07:34.572Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/23aea03b28297bc66a4461f55ae4296368a9d85fa9a454bafcb2a5348bd7/torchvision-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:46f581979c010ad6da6bd85ee602aa707e1ff44312670223b7a0ee517ad06d47", size = 4291452, upload-time = "2026-07-08T16:07:32.236Z" }, +] + +[[package]] +name = "tqdm" +version = "4.70.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/3b/6c24bec5be5e743ffd99576daa5cc077722fc7d5bbc00bd133fa0c698dc6/tqdm-4.70.0.tar.gz", hash = "sha256:55b0b0dbd97462d06ebee91e4dac24ed4d4702be82b24f07e6c1d27e08cea220", size = 795438, upload-time = "2026-07-27T11:33:15.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/1c/01bfd571a64e7f270e6bab5e33777debe0edc56759233ce84f27dec92d14/tqdm-4.70.0-py3-none-any.whl", hash = "sha256:7f585706bfddbdebf89daac705b2dfcc16890130727d3197ca62c732b4310953", size = 80184, upload-time = "2026-07-27T11:33:13.167Z" }, +] + +[[package]] +name = "transformers" +version = "5.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/3f/d89353267d511e18f137dfd7769d07837350c11b88408ce1dfe2e93e56c7/transformers-5.15.0.tar.gz", hash = "sha256:bbf98f57b2ddd7c4ecbccfa2c0069017aa6fd01cc204bd50cbc0eeadcf2a13b8", size = 9377983, upload-time = "2026-08-10T10:27:23.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/43/81355710a4c84e9420e11a86d41a5364deb561f2ef36dfdf254a07371bbb/transformers-5.15.0-py3-none-any.whl", hash = "sha256:d7f007736f67749ae9490c4f8cb5d30b452ae2d68c8675e50ba8d63ea7feb107", size = 11749280, upload-time = "2026-08-10T10:27:20.416Z" }, +] + +[[package]] +name = "triton" +version = "3.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/fa/f856e24deb462d5f18bd4b5a746957862ab9b6ee5834bda60605ec348366/triton-3.7.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9497f2e696ee368862a181a90b2dcc03ca978cc4f602abd67c7d81022a6988e1", size = 184692359, upload-time = "2026-06-17T20:03:48.288Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/fb96d15db6f36d6eae4cafb998c2e0353bf59d7c4ea1662d7497f269134a/triton-3.7.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e40869937a68206ec70d7f25bb7ec6433cb083f9135e1f36dbd318dc449a728", size = 197719725, upload-time = "2026-06-17T19:53:20.419Z" }, + { url = "https://files.pythonhosted.org/packages/00/42/c5089d4d9327fcd1e862c599cc2927f39418f84dd11a84cb2ccff9d4787a/triton-3.7.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cdbfc09d9ec58bc5e68321525653220de7515c199e7a8097a97c85e62b52cd0a", size = 184694629, upload-time = "2026-06-17T20:03:53.444Z" }, + { url = "https://files.pythonhosted.org/packages/07/42/2c3ac59253ae8892b6f307875263dd23dc875cdf732d3aea40d6d41fb7cb/triton-3.7.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58c0e131da05134a2a4788ccbcc0c1105cf0f54c8e98f19e34cd465396dc15eb", size = 197729241, upload-time = "2026-06-17T19:53:27.801Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/e01aa7ad573883ed9456f130226babdec70b005e098c4d6226a6238e761b/triton-3.7.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe4ea396a06171f1f1f58cbd39c70b09294398f7dd7c620939bab54ad6f934fa", size = 184705764, upload-time = "2026-06-17T20:03:59.064Z" }, + { url = "https://files.pythonhosted.org/packages/a4/09/5683146fda6a2b569deb78ccfd8fbfea8bfe55f726b081c0a6bb18dd6f28/triton-3.7.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2020153b08280415ec0da6607834e79166442147e78e144df06b508c75b186d2", size = 197729537, upload-time = "2026-06-17T19:53:35.516Z" }, + { url = "https://files.pythonhosted.org/packages/e9/f8/448220c3092019f9fdfab39ec47985968181d67da34b44f6a7f6280a5cbb/triton-3.7.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c58e4c61f0c73b5dba3b5d19b4a7093c32f90dc18b2a7f121a7c16ccd31107b7", size = 184814760, upload-time = "2026-06-17T20:04:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ac/229b7d4589d2e5937310e72c6d46e89599d16a4a12b479ffa1499fee8eb8/triton-3.7.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10ba85fa2cca4a2fbdeb36bf1cb082f2c252bda55bf9fccd74f65ec5bc647e68", size = 197824404, upload-time = "2026-06-17T19:53:42.772Z" }, +] + +[[package]] +name = "typer" +version = "0.27.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/4a3db7990d1f62a53182aa96eaef57aeb2886a27f90a195bc66713565d31/typer-0.27.1.tar.gz", hash = "sha256:a79bef8469a79c45498e7b814ecf8d603cc7644e9acbd9e19cac0334240b18df", size = 203994, upload-time = "2026-08-03T14:41:03.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/89/9518bc0c3929bee36b3a4a8e3daddd6e03f92f9961c66d4983b837160543/typer-0.27.1-py3-none-any.whl", hash = "sha256:53150287edd11baeb4e4722c8e394fcdf8181c0ae89485cba8d25c778d5edd56", size = 122874, upload-time = "2026-08-03T14:41:04.391Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + +[[package]] +name = "ultralytics" +version = "8.4.121" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "nvidia-ml-py" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "polars" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "ultralytics-platform" }, + { name = "ultralytics-thop" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/ba/42b7e0e162ed0d66a0e164b147090dccc68379ed4739a690cd6eb0dc5123/ultralytics-8.4.121.tar.gz", hash = "sha256:30b84fd18cc07ba3cd705fcbca5fba0684c634ed3abc003a5c6171e493f5c2e1", size = 1205904, upload-time = "2026-08-17T13:16:03.733Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/4d/6eae410efc550d1db49746ba447c5fa6bf7d3b48624eb70a918e6702d0a7/ultralytics-8.4.121-py3-none-any.whl", hash = "sha256:599fdc1268c3b0c1cb31d0bf07301ed3a27b33fba63485edf9c8b6cebf52bb37", size = 1424256, upload-time = "2026-08-17T13:15:59.978Z" }, +] + +[[package]] +name = "ultralytics-platform" +version = "0.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/53/898352d8c872aee2aacffb96b2d054d49ef06095c23c85c0f35db1fcd652/ultralytics_platform-0.1.5.tar.gz", hash = "sha256:72f6aba7f6251747c7f69d68253036d106e0142242fbc7244b4fa761e265c1ea", size = 52592, upload-time = "2026-08-15T17:22:42.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/56/684ae3e973f6fbf813714cb875eca1be09876cdfa34806e044f86b3844a1/ultralytics_platform-0.1.5-py3-none-any.whl", hash = "sha256:2e1b101b4b7a8a61757005c01101afa8cd535d653ab11f849653e17877bd14d9", size = 63884, upload-time = "2026-08-15T17:22:41.637Z" }, +] + +[[package]] +name = "ultralytics-thop" +version = "2.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/20/d6b6aaa8ecf7dbdeaf0c05f73bdcd04cf8ce468d936bc48dbcd368e75baf/ultralytics_thop-2.1.6.tar.gz", hash = "sha256:0ec2df8ebd3db35795e1f80cdc8bce6734446dbe989bca1b0c89396353f0f08c", size = 36364, upload-time = "2026-07-30T22:31:28.143Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/98/f1fa3d40d548c8a2a3eec33b7f856063bb6c7d51e16d5198b1f390b2c79d/ultralytics_thop-2.1.6-py3-none-any.whl", hash = "sha256:23f7b8ad124fa3432c1a7de9279102c4fdda699216032a7dff49f87ec3d1a3af", size = 30479, upload-time = "2026-07-30T22:31:26.874Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.52.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/28/64ca011edf31c715b4fad359c587ea52391aaffa125065695590241ff617/uvicorn-0.52.3.tar.gz", hash = "sha256:18857b9e6579300be55c91c0a1cfd37d9a2cf0cabea33b88275f199eb73b8b58", size = 100621, upload-time = "2026-08-13T16:50:02.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/2b/ebd108734a8204c6b4b93c681c9a38c5273b3ccd5d129fee4ffc1d97772c/uvicorn-0.52.3-py3-none-any.whl", hash = "sha256:116af2710dbf47c80f463cd20ee4884b6662f4c9f227d797ddc7279d2fcc2c7c", size = 79859, upload-time = "2026-08-13T16:50:01.323Z" }, +] + +[package.optional-dependencies] +standard = [ + { name = "httptools" }, + { name = "python-dotenv" }, + { name = "pyyaml" }, + { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" }, + { name = "watchfiles" }, + { name = "websockets" }, +] + +[[package]] +name = "uvloop" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" }, + { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" }, + { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" }, + { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, + { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" }, + { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" }, + { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" }, + { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" }, + { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" }, + { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" }, + { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" }, + { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" }, + { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, + { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" }, + { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" }, + { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" }, + { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" }, + { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" }, + { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" }, + { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" }, + { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" }, + { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" }, + { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" }, + { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" }, + { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" }, + { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" }, + { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" }, + { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" }, + { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" }, + { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" }, + { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" }, + { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" }, + { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" }, + { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" }, + { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" }, + { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" }, + { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" }, + { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" }, + { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" }, + { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" }, + { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" }, + { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" }, + { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" }, + { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" }, + { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" }, + { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" }, + { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" }, + { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" }, + { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/41/aa4bf9664e4cda14c3b39865b12251e8e7d239f4cd0e3cc1b6c2ccde25c1/websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98", size = 70576, upload-time = "2025-10-07T21:16:36.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/db/b10e48aa8fff7407e67470363eac595018441cf32d5e1001567a7aeba5d2/websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef", size = 82616, upload-time = "2025-10-07T21:16:34.951Z" }, +] + +[[package]] +name = "websockets" +version = "17.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/96/e01084f83a64bcb3a27994bd0cb0db68ff29d9c6707fae37ec19b18ba990/websockets-17.0.1.tar.gz", hash = "sha256:5baa9bc0dfbae8c507e51c8cf1b6d4628086f7a87bbd3a9952bd5f035451f1cc", size = 183298, upload-time = "2026-07-31T11:31:27.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ff/6199a52d864215750af8668d84b0274775011a90052081f5a9495807a92b/websockets-17.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:10f461191125c63902ea7394ae9e752b1b5785641850c1d365bb30b0f88bc53f", size = 212603, upload-time = "2026-07-31T11:29:30.771Z" }, + { url = "https://files.pythonhosted.org/packages/54/7e/439a962bcada88dcf586da77a1b2385f91e2d2910e9359540934c827156b/websockets-17.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cffc84ddec6da7f447677266fee2a3c40ecc78172f00752aa1150b8a8d65df1d", size = 210286, upload-time = "2026-07-31T11:29:32.157Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/123660edc759c225626b3b91952c7625f85c77a8362acbc35a4623120f7d/websockets-17.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c23e532c8a2325a1e7486de8763a60dc43e83f01bcaeca07e3ba79652c156db1", size = 210549, upload-time = "2026-07-31T11:29:33.388Z" }, + { url = "https://files.pythonhosted.org/packages/cd/2f/2940e57080cf56f28190287516400126d5a76b52b9a61dc10ba6f6400dbe/websockets-17.0.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c09e097d0e46e3c289bedab9a475ae344b70c30ff5646e46af22b4e6fdc97b21", size = 219874, upload-time = "2026-07-31T11:29:34.608Z" }, + { url = "https://files.pythonhosted.org/packages/42/28/9ec976c16d63cc51c28dfec74b66854048c0b8b6579946e902f91b69e8bf/websockets-17.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f47b0815af3948ec6a440b3afa02f05b18cc0939549e91b5c677b5d9c2c8472a", size = 220150, upload-time = "2026-07-31T11:29:35.831Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a4/850c699a16bbc451723856360c59bd997bec075e637154f3fa96e80d5760/websockets-17.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8848c207049ad49d318e5f64a3d4d7bb189f8328d0d98e65647788f2a085785c", size = 221389, upload-time = "2026-07-31T11:29:37.189Z" }, + { url = "https://files.pythonhosted.org/packages/47/e1/f60a891c1a4b3420d5052333a84eb7241e1fb4a71866dec1562f5fa30027/websockets-17.0.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2604de7228506b13a44a256a9d223943340c0e725af5d367dc068e192b027761", size = 224169, upload-time = "2026-07-31T11:29:38.61Z" }, + { url = "https://files.pythonhosted.org/packages/26/fb/e2a893be6fae4fddfe50ddc3035a331d3f381103d5467b7900026bdb3a64/websockets-17.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07abc3bd196a48af476a82fd47f3f79a6a3f70937a9f930cef703cfa0c9d83b6", size = 222025, upload-time = "2026-07-31T11:29:39.897Z" }, + { url = "https://files.pythonhosted.org/packages/d9/72/e3144b2d79276fab9798ed7d4aea2f0847434f186800b6f56a1eddcb3114/websockets-17.0.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:769ce7e2acfd9a89f2bed3a9c0da229459516bbc00bd4c9e2ca492c613ae4861", size = 220779, upload-time = "2026-07-31T11:29:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/c0/5e/69c02174fbcf1c40c6adc45d3c316a401558392fe7bab8969ef8c46f1689/websockets-17.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:07d78a509c3333f5908c83d7f78144ea68a6c9ec28110f5c54d81d8fcdc262c4", size = 218053, upload-time = "2026-07-31T11:29:42.322Z" }, + { url = "https://files.pythonhosted.org/packages/b3/09/7574778b095b99cfa0856583462f56568df784f9b41485145169b2ec9c64/websockets-17.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ffad64ce7ad3703d652a3fd9af26238377d24ce52c6ad8ff35d26d82f61f493f", size = 220825, upload-time = "2026-07-31T11:29:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e6/f46571f38765dbc4cbc0d0b47de8db65768006dbbd4340e6f5f51bc1d895/websockets-17.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e95e321d0d763f2b6633512605f6112ebd70d5746f3ce05c941909d4a25233f2", size = 219427, upload-time = "2026-07-31T11:29:44.731Z" }, + { url = "https://files.pythonhosted.org/packages/9d/31/6ff1fee057bd7e9dd5237fc064a749615378d003aa045b5bfc2d12b2f4f7/websockets-17.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cd526c8228e759c1006c4b7c9ac71dc4e925ced1a6a6a5a8e94643709738f63e", size = 220198, upload-time = "2026-07-31T11:29:45.997Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/d41847227a44b9ad87c3d5a9fddbfad8b7c4d6032878d8460d9d37c2d44f/websockets-17.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8cd3369e42c0246afaf9d669cfc19797e3a49e8c0a639544459c57597108b966", size = 221304, upload-time = "2026-07-31T11:29:47.314Z" }, + { url = "https://files.pythonhosted.org/packages/63/30/21a7e326c6ad2eb526cd5b816383d59cdeb28b8805b65a543c3cfbd8e8ce/websockets-17.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b580794e926cab7ff42ee4371ef14e0b22cb2bb722a607f77769136468f49a3f", size = 218858, upload-time = "2026-07-31T11:29:48.587Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/55b0331cd5bec9ce29748f79edc00075805450a47011d0c8e3b1c61dbf04/websockets-17.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5033ffe6804dd53afafa7d08e8c3eef2d2431f34d58ca30507a8442dd04a033a", size = 219840, upload-time = "2026-07-31T11:29:49.791Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/2e8bc06e546f49b32a58a2bc2957902d1809ecc37552d3d7ccd6639a126e/websockets-17.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6db9e5bf3649ab506c6ae8a3ac85a00fb1ae3816d75962771b2df8adbc5d40d2", size = 220116, upload-time = "2026-07-31T11:29:51.026Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ad/4bac01fa41aca54307157b9c9f68b066a6bb51fb18716ba618078a67b283/websockets-17.0.1-cp312-cp312-win32.whl", hash = "sha256:bc0bca48ba24c6c866847fd20478a51dd547fa0ad258dab9615c414ec534bbc0", size = 213050, upload-time = "2026-07-31T11:29:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/82/d8/c3a78cccc74a554780e9e76e323d5cde891048627025f0f82623e22dc3df/websockets-17.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2b3f3020171202b135ca078e20434977c6b2b02af647130d6980c9e39b9462e3", size = 213348, upload-time = "2026-07-31T11:29:53.891Z" }, + { url = "https://files.pythonhosted.org/packages/7b/25/e1b8824bd632c8a5a62d504b61e9e35e470b67e4be0206f5c28f90c7f86d/websockets-17.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:41d6aa06b5ab832aee72fedf47a149535b121ac900b6bb4d3fe14712afac9a79", size = 213276, upload-time = "2026-07-31T11:29:55.299Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a8/79c577bc2f874ee22f6f5ccdab97ba9ce6b96806be3fcc3a6d8490f88a21/websockets-17.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:55b12e47dcee83673a40d07686cfb6f9d6dfc285976ade9463f61d2bef3fad22", size = 212593, upload-time = "2026-07-31T11:29:56.518Z" }, + { url = "https://files.pythonhosted.org/packages/db/99/e1cfaf419bb3b2fcfd6792a846f1d936293132b0b9a56530ced016c83c7b/websockets-17.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c118a6b0e25bfc9a6802075d748fa6321714ffbdf3c88d29d9a0e3c7386c75", size = 210280, upload-time = "2026-07-31T11:29:57.768Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ef/cc994494bf7d97e41833f6ff55c24f535e4d527a10370b9631737e9c2f00/websockets-17.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:734d20364dc2cfe03674883cafcf580b6e431c5ce42b476312b9285310230cf9", size = 210538, upload-time = "2026-07-31T11:29:59.021Z" }, + { url = "https://files.pythonhosted.org/packages/87/32/fbf2d132f63ba3e67f675bccf333469786a24e0418969ce1d8e6ff9e6f02/websockets-17.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9493314a99e599163c854fb5900ad7f7ea38c5cb9d9103aa30b3c6b8181c01fa", size = 219925, upload-time = "2026-07-31T11:30:00.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/64eee3d25a47fe744a9490e0627cc373dca096755db740f91c28bd61cd35/websockets-17.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:18ded646ce98cdd3c0235825b3252f1df55765ba49b616bb10282f758667b4d0", size = 220206, upload-time = "2026-07-31T11:30:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/15/56/10ed4bc4dd75f204e3c62bd4898e44a8742a27773c80b188cfa7888aad2d/websockets-17.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1bec5d6a19f5fbe87e4940739cfc65e7bb53d8b353e1029b8037a1653b321bc", size = 221445, upload-time = "2026-07-31T11:30:02.788Z" }, + { url = "https://files.pythonhosted.org/packages/bf/be/bb14328614c068ab09569962fbf218fc00413ce3febc6d2684c764b6f37e/websockets-17.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:872273e629ca7e3d35f16a2dc6ede84e1d5c831e616b8277de6e4f83114e7c58", size = 222887, upload-time = "2026-07-31T11:30:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/32/1b/4cb0eec2fee310007104687493175af190019f705940c864f9c523fe9f6f/websockets-17.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1df81d174c1561292de9e40b141cafc04f69077272f6c352afe1d743e20810df", size = 222072, upload-time = "2026-07-31T11:30:05.258Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/14e6391de777ddb39c439c450deb551406d445e25a5877d6fa25c49d4544/websockets-17.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:759adeb5b0c5775b563254ec63b5b79089fc0045b479143a0b1b8c0ebaae1253", size = 220826, upload-time = "2026-07-31T11:30:06.53Z" }, + { url = "https://files.pythonhosted.org/packages/cb/57/96e94e384442247bbed5d3ab67381c7257355c2d66b62c3ad33a17f5d385/websockets-17.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d99db29b5444e3982f1ce2ba8a833508ad44b2f1fbd0bd99e81d825c0b461", size = 218107, upload-time = "2026-07-31T11:30:07.766Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8c/9c9dedd14c3919435df9b35cdee7111268c751252b87652f3a6a4f56e760/websockets-17.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:02ed63bf26dda9fa27df730a41f6664586c4ee05972c8fb667ce1725b3fd13d3", size = 220889, upload-time = "2026-07-31T11:30:09.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/4d/ca73c2ac82c00f50c529784bacb323e42da4816333211bc1543d90c9cf11/websockets-17.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:eab6de8a98b9a7772cf686d00b4de439fc7efb8ab05ae106ef227291d06f87c5", size = 219486, upload-time = "2026-07-31T11:30:10.289Z" }, + { url = "https://files.pythonhosted.org/packages/5b/da/fb37ac09dcd7c69dd73bac979ed393df35f78a3c232e293d1ff3bd586d24/websockets-17.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2a855b6dfe21c4d3420be265ae031829ba8ba0be0ea350d9f7c3ef30ae63ebe2", size = 220258, upload-time = "2026-07-31T11:30:11.605Z" }, + { url = "https://files.pythonhosted.org/packages/fc/04/9693f191d968a93f37326a17301a101d49580889c688f466699f89ecdee1/websockets-17.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7002d5f9e1c3ddd991cdfdbfee18cc8c8b196b2445022892badacd6cb338bbbc", size = 221358, upload-time = "2026-07-31T11:30:12.858Z" }, + { url = "https://files.pythonhosted.org/packages/cf/29/ad0d85c01db5dcf22898d51648bd2c25af0dd0a4a41c550b11acddeeeba7/websockets-17.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c395bda8e7d8f51a02e80261fb57127979e5c472675d9a96b2860619ad47da48", size = 218921, upload-time = "2026-07-31T11:30:14.064Z" }, + { url = "https://files.pythonhosted.org/packages/18/3b/bf8e855e495dcca63f2b8aa019cf2ada3160e1fa66d833c7417f3b1f7f38/websockets-17.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aadc298969ad229d8e3029fc5cc751fdad286696230f9cf014e90ff9cd8e6ea0", size = 219871, upload-time = "2026-07-31T11:30:15.358Z" }, + { url = "https://files.pythonhosted.org/packages/3b/db/c7abd6639a93a40279cd1ddc57e09e1c4f8381c4cfccdb775aa5aac9770a/websockets-17.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f11a398d8170b7ac5000baf7f258dcda579ef3ea744e0cc6a165e0dfbc0d3198", size = 220154, upload-time = "2026-07-31T11:30:16.96Z" }, + { url = "https://files.pythonhosted.org/packages/f6/2a/25a9f8f2e5a6ef34e911d2f55d9f756bdeb92b4c28cfb77b8430bbc73cb1/websockets-17.0.1-cp313-cp313-win32.whl", hash = "sha256:846a4a8b0833e3cad57523d9e3bd50ec8ea05ab9d06c582f82a1340ba096af5f", size = 213038, upload-time = "2026-07-31T11:30:18.434Z" }, + { url = "https://files.pythonhosted.org/packages/81/2f/ea1380f72bb11b64fc5bc7ae0d42de5bbf3e6dc13b965706b2a1d4e17cdf/websockets-17.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:409d93efcaa14f7a99592c5baaef5ec6ca94fba0f5aec1a86f693977c69c9c1c", size = 213348, upload-time = "2026-07-31T11:30:19.693Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c7/b956ed9151c3c74530ebc62d716fbfdbde7507a6acc6423a64f9ecfb6b8a/websockets-17.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:90246fa9e6cb192a778ce6ce024057ec54317a894db7899c922dcdc1f4cbf6a5", size = 213282, upload-time = "2026-07-31T11:30:21.045Z" }, + { url = "https://files.pythonhosted.org/packages/98/dc/cadab608924ac605647031472fb1f8792d7d4ea07565ba1899ec42028e0d/websockets-17.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:53b90c00bc6201ab6695c7ff51a04d0e425514c37515e9eeecd2c1b978ac6c0e", size = 212640, upload-time = "2026-07-31T11:30:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/7a/b034d13ca181211bbd58bb50835cb196a7784cd505b5a2079d4d03374f9f/websockets-17.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f33a649bfcb8312524173cc4bbafa7dbb236e18eee9aa31a1d324ca0ddda28c", size = 210332, upload-time = "2026-07-31T11:30:23.608Z" }, + { url = "https://files.pythonhosted.org/packages/2f/4d/943ede39b53744768edf1ed84a3f9401527388228a3d6c1249c02c3d6bd7/websockets-17.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cddc675ec31bca65473321f9a9794e488b43b3b8de5d02c8ef4810c5d5792163", size = 210546, upload-time = "2026-07-31T11:30:24.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/88dc159d2ae66743c669443246243f28d873b0c5e58271b8cc1ca0440334/websockets-17.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b3ff0ad440ad52dda64138f16895f66403f40192365e39b1010e889f289746b0", size = 219928, upload-time = "2026-07-31T11:30:26.221Z" }, + { url = "https://files.pythonhosted.org/packages/fe/f2/ff27eaefa15851a5cf7f004ab827a022bf2d6632cb520f89cb100db7e84b/websockets-17.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:72d7f2a5aeb4e82daa4ee18f125b4277f427033359be5c745ad709608446cc2c", size = 220279, upload-time = "2026-07-31T11:30:27.49Z" }, + { url = "https://files.pythonhosted.org/packages/19/2e/a5166149f363d2449c1cb2dde6486a245521979509d53b87a09f3e79662b/websockets-17.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6fd88365da261c53d3e943fb37e0d0721b9cde119f6b2e3fc84369b6ab234d63", size = 221525, upload-time = "2026-07-31T11:30:28.872Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/f46931269b3ff3bde65d27c65ddb22f9bb8ce92ac2c6c4df0910128f6219/websockets-17.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ab9f962a5b64a5c3c845d556b7dc4e6fb683f7b67179f8205e814bb2e0213ffe", size = 222897, upload-time = "2026-07-31T11:30:30.164Z" }, + { url = "https://files.pythonhosted.org/packages/42/f4/deccf3439f35df953ec35e13fe07986821c5f1ab5785d69614283bdb9034/websockets-17.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c07f145d0b9e90cbd96035f31fb79199aef4da1872854e36ebeb258e3d57594", size = 222129, upload-time = "2026-07-31T11:30:31.489Z" }, + { url = "https://files.pythonhosted.org/packages/35/a5/e1b57a59da92ade37fd021567a17b518ea8267b28e5530075844cdb525fe/websockets-17.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9f7747d3daa41a11f25f7cca5dc988fc51da97b311bed4c9d843860f79779283", size = 220875, upload-time = "2026-07-31T11:30:32.805Z" }, + { url = "https://files.pythonhosted.org/packages/f0/30/e7d0889c790a854156de424575fd67af79ddbaed9ff3157ae863dfd1c1dc/websockets-17.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2abb1ba0a5133b7d2ef3c1c9f4b0c1e8a101012dce0b594ab2b2888d9a64820e", size = 218160, upload-time = "2026-07-31T11:30:34.512Z" }, + { url = "https://files.pythonhosted.org/packages/09/2e/43db785d6ed9ae7594fae7b62bbc9cb4dfee2b015e06a1005f1e5ce283b6/websockets-17.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f3fd9a1f87f8f0f3f8e9f9bd0195f7516562d13f5b178db8c5784d1f60b60bed", size = 220951, upload-time = "2026-07-31T11:30:35.806Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f5/4ac3cab3d5e8a830657a822f64a8910e3803229c6783e59c3fd9a3487427/websockets-17.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2bc14b481e05e331811108daa1aeb41a5e237a5564ef2f02ec5a356a0f102f78", size = 219460, upload-time = "2026-07-31T11:30:37.273Z" }, + { url = "https://files.pythonhosted.org/packages/03/0e/c3a4020673ffc17c82cf1a467835038a196a555d3b4f2a50f0f063cf8ccc/websockets-17.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:57d2ee9b24b404ce75f3814f92073c0ed88106c950148d2427fe8d25ca254d1f", size = 220248, upload-time = "2026-07-31T11:30:38.527Z" }, + { url = "https://files.pythonhosted.org/packages/7d/87/e47a6a278cc1dfade38444c893ce18322943c25d4b780a74450d9d164be1/websockets-17.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:1b363bfd72a52c0658a3154a4cff219f15a474b35a235057d38853bf151acce7", size = 221421, upload-time = "2026-07-31T11:30:39.879Z" }, + { url = "https://files.pythonhosted.org/packages/da/8f/473d5fc4e3836e375b0233c6ef26777e6e5e3f7bfc84ccd524eae4090ed5/websockets-17.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:10b1587c599fa0f2c89154587c80e0fda98ade6c9fa8c0260a2823fb1800b685", size = 218975, upload-time = "2026-07-31T11:30:41.192Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b9/819ec2dcdf69031d7e9cab11247f3a6ff9bbc8c7c53ada1dbcb9055b227b/websockets-17.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d7d72843691f50b91127c50688df10cb72ec6f4c4b1d7e2c11ab33b16acf8e51", size = 219925, upload-time = "2026-07-31T11:30:42.524Z" }, + { url = "https://files.pythonhosted.org/packages/85/b9/6c0da301f6118502e079cf92f4e864adf28e56b3f8c0f6085076ced7b876/websockets-17.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90973a3a00f23afdfd1c9b06fb84289bf0220f247ef8a62501a1967c7af54f7b", size = 220218, upload-time = "2026-07-31T11:30:44.04Z" }, + { url = "https://files.pythonhosted.org/packages/55/f9/cba32dc9dd856565263d6272f594255bd0e2781deb8cd982c026a54760ad/websockets-17.0.1-cp314-cp314-win32.whl", hash = "sha256:599b03beb77633bffc095334338fad79cafc2b01fbd58953838130a9ae967d7b", size = 212626, upload-time = "2026-07-31T11:30:45.579Z" }, + { url = "https://files.pythonhosted.org/packages/fa/95/91cdd8c192287d7ea741f37cf7d64fdc1a14410f06f73805e428a1a590af/websockets-17.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:81ce19c6046ace11da7001781be7317bb1dc389f399af4b2ed962190f76f9add", size = 212969, upload-time = "2026-07-31T11:30:46.983Z" }, + { url = "https://files.pythonhosted.org/packages/8e/fd/8c98a1e431960661c5769ab1a4dd66494e87ab02d791cc79e51e0d9a289f/websockets-17.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:efe0ae052a8d023b87198921e8a7ce1dc7768816bcd2fbc20df171ac73a04891", size = 212850, upload-time = "2026-07-31T11:30:48.304Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/142f5186ee7dc3beee0426b998a79e223e067b7689afcaa95890d64aa800/websockets-17.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ab56439c9f74c52770690c7b2f616b3bf775cb3920453ee355ac765c032d8bbf", size = 212967, upload-time = "2026-07-31T11:30:49.688Z" }, + { url = "https://files.pythonhosted.org/packages/02/de/4b03ed316c9dee180365286c298219809ff247be39beeaaf9958b21167ab/websockets-17.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:20a92f78ac8250984ed459faa9ca48c285adbfc0038ddc3fdac6046990a9c9ed", size = 210504, upload-time = "2026-07-31T11:30:50.987Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d8/ad2b3e8f867e1e8cac3077e2f33ffb60b71bd763d6cfc71bd916f113c3bf/websockets-17.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6a434e59962a4fb9016bea327e1d14d6cd67670ecfb8942b4f4a0c24036634ce", size = 210702, upload-time = "2026-07-31T11:30:52.261Z" }, + { url = "https://files.pythonhosted.org/packages/48/18/7a77a82ce9d6f831c07b176da3942f7e71acd0f115f3ecdb1d00a040eb01/websockets-17.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2503c7e2a5049a12d5dac917a46d5d52591283a766165b8176bb167560421b38", size = 220290, upload-time = "2026-07-31T11:30:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/f5/af/43c3e3c3ea7ba4693c2181743f3221957df28bededadcbd9fc8a0661bde0/websockets-17.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:28012a54510fe8301bb893ef143cec30a2780a2d3bc20b7bbdf4379d7a63945d", size = 220573, upload-time = "2026-07-31T11:30:54.966Z" }, + { url = "https://files.pythonhosted.org/packages/1f/bd/ed48eca15725743ee7e2dc172e15c61de29e85ec98dace7b14257f366836/websockets-17.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22bd00f8bae2bccdb5dbe41e20f58ba44ca9fff0b4b561aaf39099c35da762ed", size = 221747, upload-time = "2026-07-31T11:30:56.762Z" }, + { url = "https://files.pythonhosted.org/packages/99/50/838deb7937a8225c4925dd4a977eafea473fabf444178a99de0bc7e92bb0/websockets-17.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e98ec9ec61cce5bc4b8b218322ad090b0994eb060bb04da704c62ef0a3d864e6", size = 223891, upload-time = "2026-07-31T11:30:58.127Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e9/657fb70c6eb6bcd01adfa5d2b06496e9911e1c1a8813d353b8c00f7591cd/websockets-17.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8e387adb0c692c6b5571bdeafc8ac9d1901ea30f10309134780b16ecd35e6605", size = 222317, upload-time = "2026-07-31T11:30:59.416Z" }, + { url = "https://files.pythonhosted.org/packages/07/4c/82cb722afa5428fed981331210c4c07600570db01bb1620579f655b5adaf/websockets-17.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bd1470d2c53fe53269bf5619da7725d30dd9b9693f1689f7a85eab8dea734442", size = 221047, upload-time = "2026-07-31T11:31:00.757Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/bd6d67d6bc65f0de0cb50de55dab42f256a9876a351c4736522eb168fda0/websockets-17.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:884af729b8ab50486acd94d9768c2b60914bf39b579ebba0a5cb73bfdfd61fd2", size = 218626, upload-time = "2026-07-31T11:31:02.48Z" }, + { url = "https://files.pythonhosted.org/packages/96/cb/6a372c8553976f0d8f97f5115826ed47e34b3be6b8bf0d0249af249a7416/websockets-17.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a60fa1a25cca1bcc2bf87b8d6be37a741f0a3239fb5e9cfb7a37173b68ffcf87", size = 221299, upload-time = "2026-07-31T11:31:03.795Z" }, + { url = "https://files.pythonhosted.org/packages/bc/31/f966e8472337974f74d788b3ef6c6f3b8b9a5f201efd16a843c91d269fa5/websockets-17.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:e8208f2729cba030ff872a92064c97584eeb9502f53d32a05a0f05d5a17ca6c6", size = 219789, upload-time = "2026-07-31T11:31:05.08Z" }, + { url = "https://files.pythonhosted.org/packages/57/34/404e83a6cc7b0efcac810b7041bffd72ff76900e6fd0aa45a26c92fb2ffe/websockets-17.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:54cdcaa56f5d3eafd57058f0fa4a3de93a310b43a3c4699f06efc4c0bd054a5a", size = 220678, upload-time = "2026-07-31T11:31:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/e5/70/8946188c2a68d67251859b589a3634918cf7867bf0b891347a5ecaa43d30/websockets-17.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4d41c0a1d47a478bc432b3b9068097bee1ce0c5b19327ea6f75c2ab34ab1f2fb", size = 221697, upload-time = "2026-07-31T11:31:08.023Z" }, + { url = "https://files.pythonhosted.org/packages/04/16/ee73fc2083a2938ac6209f4ec804960496835b20a0068dbcfe8424957c04/websockets-17.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f991247276797d0c61ab7770bc9791eadc16f683b4d83517f624932adc1a8bab", size = 219390, upload-time = "2026-07-31T11:31:09.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/6a/d5f88033c69932af6cdaa72da62516ade47c257e3bf69f4c0ba5f40e12a2/websockets-17.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:733e3cc7171fa1b899edbe725ef9382d0e960657dc1fd933f3281ae910c01dab", size = 220161, upload-time = "2026-07-31T11:31:10.915Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/6183dd2c0370287ecf4afe0bb33aca364208e5e7b0e1a286adcaecc0c78b/websockets-17.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:810cb3fb5fa6e447216f4e82d9a85cb8aed0929ae3538153ddfe8a6e3121a58d", size = 220591, upload-time = "2026-07-31T11:31:12.289Z" }, + { url = "https://files.pythonhosted.org/packages/26/93/70f6516d85b9744f7eac224c4b1b9ef4e84133f80b53be02080cb1c3e663/websockets-17.0.1-cp314-cp314t-win32.whl", hash = "sha256:17ac37716c0244e82c9e384c41653c090b1864c6610224ca3857e7f7b58fce10", size = 212755, upload-time = "2026-07-31T11:31:13.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/2c/9d9c1da5a7ea9af307b386d25f64d1dead4729644198d2b92e36db5dfd41/websockets-17.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bb31f42ea095ea826463c770829aa188a86c9a5c976b1467cbbf583c811de833", size = 213094, upload-time = "2026-07-31T11:31:15.367Z" }, + { url = "https://files.pythonhosted.org/packages/5b/24/a585e7573e128070605d003b5544729bcd58d9756c7e99d550818ca4b916/websockets-17.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:dbfae8e75b342e31fc6fd1a8bbb393b7cbb91d6cfd581650300a94381e7b7e2b", size = 213009, upload-time = "2026-07-31T11:31:16.776Z" }, + { url = "https://files.pythonhosted.org/packages/09/ce/3929538b2b9918f5eee623fbf3346893973191f6df93f19bbda097bd7bb7/websockets-17.0.1-py3-none-any.whl", hash = "sha256:c6be9cba65c65cc76dfa3d4619e359ff02a4476c74e179b215236c11a0b32345", size = 206718, upload-time = "2026-07-31T11:31:26.037Z" }, +] + +[[package]] +name = "yarl" +version = "1.24.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, + { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, + { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, + { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, + { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, +] diff --git a/backend/vendor/s2s-cloud/.dockerignore b/backend/vendor/s2s-cloud/.dockerignore new file mode 100644 index 0000000..af66ddd --- /dev/null +++ b/backend/vendor/s2s-cloud/.dockerignore @@ -0,0 +1,17 @@ +tmp +cache +.venv +.git +.mypy_cache +.pytest_cache +.ruff_cache +__pycache__ +**/__pycache__ +*.py[cod] +*.egg-info +dist +build +Dockerfile +docker-compose.yml +.dockerignore +.gitignore diff --git a/backend/vendor/s2s-cloud/.github/dependabot.yml b/backend/vendor/s2s-cloud/.github/dependabot.yml new file mode 100644 index 0000000..15f7bdd --- /dev/null +++ b/backend/vendor/s2s-cloud/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + cooldown: + default-days: 7 + groups: + actions: + patterns: ["*"] diff --git a/backend/vendor/s2s-cloud/.github/scripts/star_history.py b/backend/vendor/s2s-cloud/.github/scripts/star_history.py new file mode 100644 index 0000000..9f5f004 --- /dev/null +++ b/backend/vendor/s2s-cloud/.github/scripts/star_history.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +"""Fetch stargazer history for a repo and render it as a static SVG. + +Runs in CI with the automatic GITHUB_TOKEN. No third-party dependencies. + +Usage: + GITHUB_TOKEN=... python star_history.py owner/repo output.svg +""" + +import json +import math +import os +import sys +import urllib.request +from datetime import datetime, timezone + +API = "https://api.github.com/graphql" +PER_PAGE = 100 +MAX_PAGES = 400 # preserve the REST implementation's 40k-star cap +MAX_POINTS = 120 # downsample the curve to at most this many points + + +def gh_post(query: str, variables: dict, token: str) -> dict: + payload = json.dumps({"query": query, "variables": variables}).encode() + req = urllib.request.Request(API, data=payload) + req.add_header("Accept", "application/vnd.github+json") + req.add_header("Content-Type", "application/json") + req.add_header("X-GitHub-Api-Version", "2022-11-28") + if token: + req.add_header("Authorization", f"Bearer {token}") + with urllib.request.urlopen(req, timeout=30) as resp: + result = json.loads(resp.read()) + if result.get("errors"): + raise RuntimeError(json.dumps(result["errors"])) + return result + + +def fetch_star_dates(repo: str, token: str) -> list[datetime]: + owner, name = repo.split("/", 1) + query = """ + query($owner: String!, $name: String!, $cursor: String, $perPage: Int!) { + repository(owner: $owner, name: $name) { + stargazers(first: $perPage, after: $cursor) { + edges { starredAt } + pageInfo { hasNextPage endCursor } + } + } + } + """ + dates = [] + page = 1 + cursor = None + while True: + result = gh_post( + query, + { + "owner": owner, + "name": name, + "cursor": cursor, + "perPage": PER_PAGE, + }, + token, + ) + stargazers = result["data"]["repository"]["stargazers"] + for edge in stargazers["edges"]: + ts = edge.get("starredAt") + if ts: + dates.append( + datetime.strptime(ts, "%Y-%m-%dT%H:%M:%SZ").replace( + tzinfo=timezone.utc + ) + ) + page_info = stargazers["pageInfo"] + if not page_info["hasNextPage"]: + break + page += 1 + if page > MAX_PAGES: + break + cursor = page_info["endCursor"] + dates.sort() + return dates + + +def downsample(dates: list[datetime]) -> list[tuple[datetime, int]]: + n = len(dates) + points = [(dates[0], 1)] + if n > 1: + step = max(1, n // MAX_POINTS) + for i in range(step, n, step): + points.append((dates[i], i + 1)) + points.append((dates[-1], n)) + now = datetime.now(timezone.utc) + points.append((now, n)) + return points + + +def nice_ceil(v: float) -> int: + if v <= 0: + return 1 + mag = 10 ** int(math.floor(math.log10(v))) + for mult in (1, 1.2, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10): + if v <= mag * mult: + return int(mag * mult) + return int(mag * 10) + + +def render_svg(points: list[tuple[datetime, int]], repo: str) -> str: + width, height = 800, 420 + ml, mr, mt, mb = 70, 30, 50, 60 + pw, ph = width - ml - mr, height - mt - mb + + t0 = points[0][0].timestamp() + t1 = points[-1][0].timestamp() + tspan = max(t1 - t0, 1) + ymax = nice_ceil(points[-1][1] * 1.08) + + def x(ts: float) -> float: + return ml + (ts - t0) / tspan * pw + + def y(v: float) -> float: + return mt + ph - v / ymax * ph + + line = " ".join( + f"{'M' if i == 0 else 'L'}{x(d.timestamp()):.1f},{y(c):.1f}" + for i, (d, c) in enumerate(points) + ) + area = ( + line + + f" L{x(points[-1][0].timestamp()):.1f},{y(0):.1f}" + + f" L{x(points[0][0].timestamp()):.1f},{y(0):.1f} Z" + ) + + grid, ylabels = [], [] + for i in range(6): + v = ymax * i / 5 + yy = y(v) + grid.append( + f'' + ) + label = f"{v / 1000:.1f}k".replace(".0k", "k") if v >= 1000 else f"{int(v)}" + ylabels.append( + f'{label}' + ) + + xlabels = [] + for i in range(6): + ts = t0 + tspan * i / 5 + d = datetime.fromtimestamp(ts, tz=timezone.utc) + xlabels.append( + f'{d.strftime("%b %Y")}' + ) + + total = points[-1][1] + return f""" + + {repo} star history + {total:,} stars + {"".join(grid)} + {"".join(ylabels)} + {"".join(xlabels)} + + + + +""" + + +def main() -> int: + if len(sys.argv) != 3: + print(__doc__) + return 2 + repo, out = sys.argv[1], sys.argv[2] + token = os.environ.get("GITHUB_TOKEN", "") + dates = fetch_star_dates(repo, token) + if not dates: + print("no stargazers found", file=sys.stderr) + return 1 + os.makedirs(os.path.dirname(out) or ".", exist_ok=True) + svg = render_svg(downsample(dates), repo) + with open(out, "w") as f: + f.write(svg) + print(f"wrote {out} ({len(dates):,} stars)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/backend/vendor/s2s-cloud/.github/workflows/ci.yml b/backend/vendor/s2s-cloud/.github/workflows/ci.yml new file mode 100644 index 0000000..5ae14e7 --- /dev/null +++ b/backend/vendor/s2s-cloud/.github/workflows/ci.yml @@ -0,0 +1,98 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + ruff: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7.0.0 + - uses: astral-sh/setup-uv@v8.2.0 + with: + version: "latest" + - run: uv sync --group dev + - name: Ruff check + run: uv run ruff check src/ tests/ + - name: Ruff format + run: uv run ruff format --check src/ tests/ + + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7.0.0 + - uses: astral-sh/setup-uv@v8.2.0 + with: + version: "latest" + - run: uv sync --group dev + - name: Mypy + run: uv run mypy src/ + + pytest: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7.0.0 + - uses: astral-sh/setup-uv@v8.2.0 + with: + version: "latest" + - run: uv sync --group dev + - name: NLTK data (punkt_tab for sent_tokenize) + run: uv run python -c "import nltk; nltk.download('punkt_tab')" + - name: Pytest + run: uv run pytest tests/ -x -q + + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7.0.0 + - uses: astral-sh/setup-uv@v8.2.0 + with: + version: "latest" + - name: Build distributions + run: uv build + - name: Check package metadata + run: uvx twine check --strict dist/* + - uses: actions/upload-artifact@v7 + with: + name: dist + path: dist/* + + install-smoke: + name: Install smoke (${{ matrix.label }}) + needs: package + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + include: + - label: linux + os: ubuntu-latest + - label: macos-arm64 + os: macos-14 + steps: + - uses: actions/checkout@v7.0.0 + - uses: actions/setup-python@v6.3.0 + with: + python-version: "3.11" + - uses: actions/download-artifact@v8 + with: + name: dist + path: dist + - name: Install wheel with pip + shell: bash + run: | + python -m venv .venv-smoke + source .venv-smoke/bin/activate + python -m pip install --upgrade pip + python -m pip install dist/speech_to_speech-*.whl + python -m pip check + - name: Run installed CLI smoke test + shell: bash + run: | + source .venv-smoke/bin/activate + unset OPENAI_API_KEY + # Validate the installed entry point without loading models or calling OpenAI. + python tests/install_smoke.py diff --git a/backend/vendor/s2s-cloud/.github/workflows/publish.yml b/backend/vendor/s2s-cloud/.github/workflows/publish.yml new file mode 100644 index 0000000..e72931b --- /dev/null +++ b/backend/vendor/s2s-cloud/.github/workflows/publish.yml @@ -0,0 +1,48 @@ +name: Publish + +on: + push: + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + version: "latest" + enable-cache: false + - name: Build distributions + run: uv build + - name: Check package metadata + run: uvx twine check --strict dist/* + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dist + path: dist/* + + publish: + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/speech-to-speech/ + permissions: + contents: read + id-token: write + steps: + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dist + path: dist + - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1 diff --git a/backend/vendor/s2s-cloud/.github/workflows/star-history.yml b/backend/vendor/s2s-cloud/.github/workflows/star-history.yml new file mode 100644 index 0000000..fb6386e --- /dev/null +++ b/backend/vendor/s2s-cloud/.github/workflows/star-history.yml @@ -0,0 +1,38 @@ +name: Update star history chart + +on: + workflow_dispatch: + +permissions: + contents: write + +jobs: + update-chart: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.repository.default_branch }} + + - name: Generate chart + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: python3 .github/scripts/star_history.py "${{ github.repository }}" assets/star-history.svg + + - name: Commit to update branch if changed + env: + BASE_BRANCH: ${{ github.event.repository.default_branch }} + UPDATE_BRANCH: star-history-update-${{ github.run_id }}-${{ github.run_attempt }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add assets/star-history.svg + if ! git diff --cached --quiet; then + git commit -m "chore: update star history chart [skip ci]" + git push origin "HEAD:refs/heads/${UPDATE_BRANCH}" + pr_url="https://github.com/${{ github.repository }}/compare/${BASE_BRANCH}...${UPDATE_BRANCH}?expand=1" + echo "Open a PR: ${pr_url}" + echo "Open a PR: [${UPDATE_BRANCH}](${pr_url})" >> "$GITHUB_STEP_SUMMARY" + else + echo "No changes" + fi diff --git a/backend/vendor/s2s-cloud/.gitignore b/backend/vendor/s2s-cloud/.gitignore new file mode 100644 index 0000000..55db326 --- /dev/null +++ b/backend/vendor/s2s-cloud/.gitignore @@ -0,0 +1,231 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +# Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +# poetry.lock +# poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +# pdm.lock +# pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +# pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# Redis +*.rdb +*.aof +*.pid + +# RabbitMQ +mnesia/ +rabbitmq/ +rabbitmq-data/ + +# ActiveMQ +activemq-data/ + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +# .idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ + +# Streamlit +.streamlit/secrets.toml + +.mypy_cache/ +.pytype/ +.pyre/ +.dmypy.json +dmypy.json +.pytest_cache/ +.hypothesis/ +.coverage* +.coverage +cover/ +.tox/ +uv.lock \ No newline at end of file diff --git a/backend/vendor/s2s-cloud/AGENTS.md b/backend/vendor/s2s-cloud/AGENTS.md new file mode 100644 index 0000000..c1e4353 --- /dev/null +++ b/backend/vendor/s2s-cloud/AGENTS.md @@ -0,0 +1,26 @@ +# Repository Instructions + +- Never include `codex` in branch names or pull request titles. +- Keep release pull requests focused on version metadata and release documentation. +- Do not commit local build artifacts such as `dist/`, `build/`, or generated wheel/sdist files. + +## Publishing to PyPI + +PyPI publishing is handled by GitHub Actions in `.github/workflows/publish.yml`. The workflow runs on pushed tags that match `v*`, builds the package with `uv build`, checks the artifacts with `twine check --strict`, and publishes through the configured `pypi` environment. + +To prepare a release: + +1. Confirm the intended version is not already published on PyPI. +2. Bump `version` in `pyproject.toml`. +3. Bump `__version__` in `src/speech_to_speech/__init__.py`. +4. Open and merge a pull request with only the release preparation changes. + +To publish after the release PR is merged: + +1. Update `main` locally: `git checkout main && git pull origin main`. +2. Create an annotated tag for the version: `git tag -a vX.Y.Z -m "Release vX.Y.Z"`. +3. Push the tag: `git push origin vX.Y.Z`. +4. Watch the `Publish` GitHub Actions workflow complete successfully. +5. Verify the new version appears at `https://pypi.org/project/speech-to-speech/`. + +Only upload manually if the GitHub Actions workflow is unavailable and the maintainers have explicitly chosen that fallback. diff --git a/backend/vendor/s2s-cloud/Dockerfile b/backend/vendor/s2s-cloud/Dockerfile new file mode 100644 index 0000000..99d58f7 --- /dev/null +++ b/backend/vendor/s2s-cloud/Dockerfile @@ -0,0 +1,27 @@ +FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 +ENV PATH="/usr/src/app/.venv/bin:${PATH}" + +WORKDIR /usr/src/app + +# Install packages +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + libportaudio2 \ + libsndfile1 \ + python3 \ + python3-pip \ + python3-venv \ + && rm -rf /var/lib/apt/lists/* +RUN python3 -m pip install --no-cache-dir --break-system-packages uv + +COPY pyproject.toml README.md LICENSE MANIFEST.in ./ +RUN uv sync --python /usr/bin/python3 --no-install-project --no-dev + +COPY . . +RUN uv sync --python /usr/bin/python3 --no-dev +RUN python -c "import nltk; nltk.download('punkt_tab'); nltk.download('averaged_perceptron_tagger_eng')" diff --git a/backend/vendor/s2s-cloud/Dockerfile.arm64 b/backend/vendor/s2s-cloud/Dockerfile.arm64 new file mode 100644 index 0000000..948574b --- /dev/null +++ b/backend/vendor/s2s-cloud/Dockerfile.arm64 @@ -0,0 +1,28 @@ +ARG BASE_PLATFORM=linux/arm64 +FROM --platform=$BASE_PLATFORM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV PYTHONUNBUFFERED=1 +ENV PATH="/usr/src/app/.venv/bin:${PATH}" + +WORKDIR /usr/src/app + +# Install packages +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + libportaudio2 \ + libsndfile1 \ + python3 \ + python3-pip \ + python3-venv \ + && rm -rf /var/lib/apt/lists/* +RUN python3 -m pip install --no-cache-dir --break-system-packages uv + +COPY pyproject.toml README.md LICENSE MANIFEST.in ./ +RUN uv sync --python /usr/bin/python3 --no-install-project --no-dev + +COPY . . +RUN uv sync --python /usr/bin/python3 --no-dev +RUN python -c "import nltk; nltk.download('punkt_tab'); nltk.download('averaged_perceptron_tagger_eng')" diff --git a/backend/vendor/s2s-cloud/LICENSE b/backend/vendor/s2s-cloud/LICENSE new file mode 100644 index 0000000..48999a3 --- /dev/null +++ b/backend/vendor/s2s-cloud/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [2024] [The HuggingFace Inc. team] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/backend/vendor/s2s-cloud/MANIFEST.in b/backend/vendor/s2s-cloud/MANIFEST.in new file mode 100644 index 0000000..054ec2d --- /dev/null +++ b/backend/vendor/s2s-cloud/MANIFEST.in @@ -0,0 +1,4 @@ +prune tests +global-exclude __pycache__ +global-exclude *.py[cod] +global-exclude .DS_Store diff --git a/backend/vendor/s2s-cloud/README.md b/backend/vendor/s2s-cloud/README.md new file mode 100644 index 0000000..f46635d --- /dev/null +++ b/backend/vendor/s2s-cloud/README.md @@ -0,0 +1,606 @@ +
+
 
+ + +# Speech To Speech: Build voice agents with open-source models + +[![PyPI](https://img.shields.io/pypi/v/speech-to-speech)](https://pypi.org/project/speech-to-speech/) +[![Python](https://img.shields.io/pypi/pyversions/speech-to-speech)](https://pypi.org/project/speech-to-speech/) +[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](./LICENSE) + +
+ +A low-latency, fully modular voice-agent pipeline: **VAD -> STT -> LLM -> TTS**, exposed through an **OpenAI Realtime-compatible WebSocket API**. Every component is swappable. The LLM slot speaks OpenAI-compatible protocols, so you can point it at a hosted provider, at [HF Inference Providers](https://huggingface.co/inference-providers), or at a vLLM or llama.cpp server on your own hardware for a fully local, fully open stack. + +This pipeline runs in production as the conversation backend for thousands of [Reachy Mini](https://huggingface.co/blog/reachy-mini) robots. + +

+ + + + Switching an OpenAI Realtime client endpoint from hosted OpenAI to a self-hosted speech-to-speech server + +

+ +## Quickstart + +```bash +pip install speech-to-speech +export OPENAI_API_KEY=... +speech-to-speech +``` + +This starts an OpenAI Realtime-compatible server at `ws://localhost:8765/v1/realtime` using Parakeet TDT for local STT, an OpenAI-compatible LLM, and Qwen3-TTS for local speech output. + +From a source checkout, talk to it from a second terminal: + +```bash +python scripts/listen_and_play_realtime.py --host 192.168.1.9 --port 8765 +``` + +Prefer to keep the LLM on your own machine? Serve Gemma 4 with llama.cpp: + +```bash +llama-server -hf ggml-org/gemma-4-E4B-it-GGUF -np 2 -c 65536 -fa on --swa-full +``` + +Then point the OpenAI-compatible LLM backend at it: + +```bash +speech-to-speech \ + --model_name "ggml-org/gemma-4-E4B-it-GGUF" \ + --responses_api_base_url "http://192.168.1.9:8080/v1" \ + --responses_api_api_key "" +``` + +Any OpenAI Realtime-compatible client can connect. See [Realtime API](#realtime-api) for the protocol and [LLM backends](#llm-backends) for provider and local-server options. + +## Index + +* [How it works](#how-it-works) +* [Installation](#installation) +* [Supported components](#supported-components) +* [Run modes](#run-modes) +* [Realtime API](#realtime-api) +* [LLM backends](#llm-backends) +* [Multi-language support](#multi-language-support) +* [Pocket TTS](#pocket-tts) +* [CLI reference](#cli-reference) +* [Contributing](#contributing) +* [Star history](#star-history) +* [Citations](#citations) + +## How it works + +The pipeline is a cascade of four components, each running in its own thread and connected by queues: + +1. **Voice Activity Detection (VAD)**: [Silero VAD v5](https://github.com/snakers4/silero-vad) detects speech boundaries and turn-taking. +2. **Speech to Text (STT)**: transcribes the user's turn, with optional live partial transcripts. +3. **Language Model (LLM)**: generates the response, streaming text and tool calls. +4. **Text to Speech (TTS)**: synthesizes audio and streams it back to the client. + +Every stage has multiple interchangeable backends, selected via CLI flags. The code is designed for easy modification, with a focus on models available through Transformers and the Hugging Face Hub. + +## Installation + +Requires Python 3.10+. + +```bash +pip install speech-to-speech +``` + +The default install covers the standard realtime path: + +- Parakeet TDT for STT +- OpenAI-compatible API for the language model +- Qwen3-TTS for speech output, using the GGML backend by default on non-macOS platforms and `mlx-audio` on Apple Silicon +- local audio and realtime server modes + +macOS and non-macOS dependencies are resolved automatically via platform markers in `pyproject.toml`. + +### CUDA Note for Qwen3-TTS + +On Linux, the Qwen3-TTS GGML backend comes from `faster-qwen3-tts[ggml]`. Its default `qwentts-cpp-python` wheel on PyPI targets CUDA 12.8. If your machine does not have the CUDA 12 runtime that wheel expects, install the matching wheel from the Hugging Face wheelhouse before installing `speech-to-speech`: + +```bash +# CUDA 13.x +pip install "qwentts-cpp-python==0.3.1+cu130" \ + -f https://huggingface.co/datasets/andito/qwentts-cpp-python-wheels/tree/main/whl/cu130 + +# CUDA 12.4 +pip install "qwentts-cpp-python==0.3.1+cu124" \ + -f https://huggingface.co/datasets/andito/qwentts-cpp-python-wheels/tree/main/whl/cu124 + +# CPU-only fallback +pip install "qwentts-cpp-python==0.3.1+cpu" \ + -f https://huggingface.co/datasets/andito/qwentts-cpp-python-wheels/tree/main/whl/cpu + +pip install speech-to-speech +``` + +To use the previous CUDA-graphs implementation instead of GGML, pass `--qwen3_tts_backend torch`. + +### Optional Backends + +Extra backends are installed with pip extras: + +```bash +pip install "speech-to-speech[kokoro]" # Kokoro-82M TTS on non-macOS +pip install "speech-to-speech[pocket]" # Pocket TTS +pip install "speech-to-speech[chattts]" # ChatTTS +pip install "speech-to-speech[facebook-mms]" # MMS TTS +pip install "speech-to-speech[faster-whisper]" # Faster Whisper STT +pip install "speech-to-speech[whisper-mlx]" # Lightning Whisper MLX STT on macOS +pip install "speech-to-speech[paraformer]" # Paraformer STT through FunASR +pip install "speech-to-speech[mlx-lm]" # mlx-vlm support for vision models on macOS +``` + +Deprecated implementations, including MeloTTS, live in [`archive/`](./archive) and are no longer wired into the CLI. + +**Note on DeepFilterNet:** DeepFilterNet, used for optional audio enhancement in VAD, requires `numpy<2` and conflicts with Pocket TTS, which requires `numpy>=2`. Install it manually only in environments where you are not using Pocket TTS. + +### From Source + +```bash +git clone https://github.com/huggingface/speech-to-speech.git +cd speech-to-speech +uv sync +``` + +This installs the package in editable mode and makes the `speech-to-speech` CLI available. + +## Supported Components + +| Component | Backend | Platforms | Install | +|---|---|---|---| +| VAD | [Silero VAD v5](https://github.com/snakers4/silero-vad) | all | built-in | +| STT | [Parakeet TDT](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3) (default) | CUDA / CPU through nano-parakeet, Apple Silicon through MLX | built-in | +| STT | [Whisper](https://huggingface.co/docs/transformers/en/model_doc/whisper) through Transformers | CUDA / CPU | built-in | +| STT | [Faster Whisper](https://github.com/SYSTRAN/faster-whisper) | CUDA / CPU | `faster-whisper` | +| STT | [Lightning Whisper MLX](https://github.com/mustafaaljadery/lightning-whisper-mlx) | Apple Silicon | `whisper-mlx` | +| STT | [MLX Audio Whisper](https://github.com/huggingface/mlx-audio) | Apple Silicon | built-in on macOS | +| STT | [Paraformer](https://github.com/modelscope/FunASR) | CUDA / CPU | `paraformer` | +| LLM | OpenAI-compatible API (`responses-api`, `chat-completions`) | hosted providers or self-hosted servers | built-in | +| LLM | [Transformers](https://huggingface.co/models?pipeline_tag=text-generation&sort=trending) | CUDA / CPU | built-in | +| LLM | [mlx-lm](https://github.com/ml-explore/mlx-lm) | Apple Silicon | built-in on macOS | +| TTS | [Qwen3-TTS](https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice) (default) | GGML / CUDA on Linux, mlx-audio on macOS | built-in | +| TTS | [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) | CUDA / CPU, Apple Silicon | `kokoro` on non-macOS; built-in on macOS | +| TTS | [Pocket TTS](https://github.com/kyutai-labs/pocket-tts) | CPU / CUDA | `pocket` | +| TTS | [ChatTTS](https://github.com/2noise/ChatTTS) | CUDA / CPU | `chattts` | +| TTS | [MMS TTS](https://huggingface.co/docs/transformers/model_doc/mms) | CUDA / CPU | `facebook-mms` | + +Select implementations with `--stt`, `--llm_backend`, and `--tts`. Run `speech-to-speech -h` for exact values and backend-specific flags. + +## Run Modes + +| Mode | Transport | Use it when | +|---|---|---| +| `realtime` (default) | WebSocket, OpenAI Realtime protocol at `/v1/realtime` | You are building an app or device against a standard voice API. | +| `local` | Your machine's microphone and speakers | You want to talk to the pipeline directly, no client needed. | +| `websocket` | Raw PCM over WebSocket | You want a minimal custom client without the Realtime protocol. | +| `socket` | Raw PCM over TCP | Models run on a remote server, with a simple microphone/playback client. | + +### Realtime Server + +```bash +export OPENAI_API_KEY=... +speech-to-speech +``` + +This is equivalent to: + +```bash +speech-to-speech \ + --thresh 0.6 \ + --stt parakeet-tdt \ + --llm_backend responses-api \ + --tts qwen3 \ + --qwen3_tts_model_name Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \ + --qwen3_tts_speaker Aiden \ + --qwen3_tts_language auto \ + --qwen3_tts_backend ggml \ + --qwen3_tts_non_streaming_mode True \ + --qwen3_tts_mlx_quantization 6bit \ + --model_name gpt-5.4-mini \ + --chat_size 30 \ + --responses_api_stream \ + --enable_live_transcription \ + --mode realtime +``` + +The default model is `gpt-5.4-mini` through the OpenAI Responses API. Override it with `--model_name`, and set `--responses_api_base_url` for another OpenAI-compatible provider or server. + +### Local Mac + +```bash +speech-to-speech --local_mac_optimal_settings +``` + +Optionally with a specific LLM: + +```bash +speech-to-speech \ + --local_mac_optimal_settings \ + --model_name mlx-community/Qwen3-4B-Instruct-2507-bf16 +``` + +This setting: + +- Adds `--device mps` to use MPS for all models. +- Sets Parakeet TDT for STT. +- Sets MLX LM as the LLM backend. +- Sets Qwen3-TTS for TTS, using `mlx-audio` with the `6bit` MLX variant by default. +- Sets `--mode local`. + +`--tts pocket` and `--tts kokoro` are also valid on macOS. + +To compare the MLX quantization variants locally: + +```bash +python scripts/benchmark_tts.py \ + --handlers qwen3 \ + --iterations 3 \ + --qwen3_mlx_quantizations bf16 4bit 6bit 8bit +``` + +### WebSocket + +1. Run the pipeline in WebSocket mode: + + ```bash + speech-to-speech --mode websocket --ws_host 0.0.0.0 --ws_port 8765 + ``` + +2. Connect from your client at `ws://:8765`. Send raw audio bytes as 16 kHz, int16, mono PCM and receive generated audio bytes back. + +### TCP Socket + +TCP socket mode is intentionally minimal. It streams raw PCM audio, but does not provide the full Realtime API feature set, including interruption handling, live transcript events, or tool-call events. + +1. Run the pipeline on the server: + + ```bash + speech-to-speech --mode socket --recv_host 0.0.0.0 --send_host 0.0.0.0 + ``` + +2. Run the client locally to handle microphone input and playback: + + ```bash + python scripts/listen_and_play.py --host + ``` + +### Docker + +Install the [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html), then: + +```bash +docker compose up +``` + +The compose file starts a llama.cpp server with Gemma 4, starts the TCP socket server, and exposes ports `8080`, `12345`, and `12346`. + +## Realtime API + +Realtime mode streams audio over a WebSocket using the OpenAI Realtime protocol, with live transcription and low-latency turn-taking. The server exposes `/v1/realtime`, and any OpenAI Realtime-compatible client can connect: + +```python +from openai import OpenAI + +client = OpenAI( + base_url="http://localhost:8765/v1", + websocket_base_url="ws://localhost:8765/v1", + api_key="not-needed", +) + +with client.realtime.connect(model="local") as conn: + conn.send( + { + "type": "session.update", + "session": { + "type": "realtime", + "instructions": "You are a helpful assistant.", + "audio": { + "input": { + "turn_detection": { + "type": "server_vad", + "interrupt_response": True, + } + } + }, + }, + } + ) + + for event in conn: + print(event.type) +``` + +The server implements the core Realtime event set: `input_audio_buffer.append`, `session.update`, `conversation.item.create`, `response.create`, and `response.cancel` inbound; speech start/stop, streaming transcription, audio deltas, tool calls, and `response.done` outbound. The full event reference, architecture, and design details live in the [Realtime Engine README](./src/speech_to_speech/api/openai_realtime/README.md). + +## LLM Backends + +The LLM is the most compute-intensive and highest-latency component in the pipeline. A single forward pass through a large model can dominate end-to-end response time, so choosing the right backend for your hardware and latency budget matters. The pipeline supports: + +- **Local inference**: `transformers` on CUDA / CPU and `mlx-lm` on Apple Silicon. +- **Self-hosted servers**: `responses-api` and `chat-completions` can point at a local [vLLM](https://github.com/vllm-project/vllm) or [llama.cpp](https://github.com/ggerganov/llama.cpp) server. +- **Provider APIs**: the same backends work with OpenAI, [HF Inference Providers](https://huggingface.co/inference-providers), [OpenRouter](https://openrouter.ai), and other OpenAI-compatible providers. + +Two API backends are available, sharing the same `--responses_api_*` connection flags: + +- `--llm_backend responses-api` (default) targets `/v1/responses`. +- `--llm_backend chat-completions` targets `/v1/chat/completions`. + +The examples below pair Parakeet TDT for local STT and Qwen3-TTS for local TTS with different LLM backends. + +### Responses API Backend + +Works with any provider or server that implements the OpenAI Responses API. Point `--responses_api_base_url` at the endpoint and set `--model_name` accordingly: + +| Provider / server | `--responses_api_base_url` | `--responses_api_api_key` | +|---|---|---| +| OpenAI | omit, uses OpenAI default | `$OPENAI_API_KEY` | +| HF Inference Providers | `https://router.huggingface.co/v1` | `$HF_TOKEN` | +| OpenRouter | `https://openrouter.ai/api/v1` | `$OPENROUTER_API_KEY` | +| vLLM | `http://localhost:8000/v1` | omit or any string | +| llama.cpp | `http://192.168.1.9:8080/v1` | empty string | + +```bash +# OpenAI +speech-to-speech \ + --mode local \ + --stt parakeet-tdt \ + --llm_backend responses-api \ + --tts qwen3 \ + --qwen3_tts_mlx_quantization 6bit \ + --model_name "gpt-4o-mini" \ + --responses_api_api_key "$OPENAI_API_KEY" \ + --responses_api_stream \ + --enable_live_transcription +``` + +```bash +# HF Inference Providers: Qwen3.5-9B via Together +speech-to-speech \ + --mode local \ + --stt parakeet-tdt \ + --llm_backend responses-api \ + --tts qwen3 \ + --qwen3_tts_mlx_quantization 6bit \ + --model_name "Qwen/Qwen3.5-9B:together" \ + --responses_api_base_url "https://router.huggingface.co/v1" \ + --responses_api_api_key "$HF_TOKEN" \ + --responses_api_stream \ + --enable_live_transcription +``` + +```bash +# HF Inference Providers: GPT-oss-20B via Groq +speech-to-speech \ + --stt parakeet-tdt \ + --llm_backend responses-api \ + --tts qwen3 \ + --qwen3_tts_mlx_quantization 6bit \ + --model_name "openai/gpt-oss-20b:groq" \ + --responses_api_base_url "https://router.huggingface.co/v1" \ + --responses_api_api_key "$HF_TOKEN" \ + --responses_api_stream \ + --enable_live_transcription +``` + +### Chat Completions Backend + +Identical configuration to `responses-api`, reusing the same `--responses_api_*` connection flags, but talks to `/v1/chat/completions` instead of `/v1/responses`. Prefer it when: + +- the provider ignores `chat_template_kwargs.enable_thinking` on the Responses path and needs a `reasoning_effort` knob to suppress reasoning, or +- the server's Responses streaming tool-call path is unreliable, while its Chat Completions tool-call streaming is solid. This is useful for some vLLM builds; see [#312](https://github.com/huggingface/speech-to-speech/issues/312). + +Add `--responses_api_reasoning_effort none` to disable reasoning on providers where the chat-template flag has no effect: + +```bash +# vLLM serving a Qwen model with tool calling +speech-to-speech \ + --mode realtime \ + --stt parakeet-tdt \ + --llm_backend chat-completions \ + --tts qwen3 \ + --model_name "Qwen/Qwen3-4B-Instruct-2507" \ + --responses_api_base_url "http://localhost:8000/v1" \ + --responses_api_stream +``` + +```bash +# Gemma 4 31B via the HF router on Cerebras, with reasoning disabled for low voice latency +speech-to-speech \ + --mode realtime \ + --stt parakeet-tdt \ + --llm_backend chat-completions \ + --tts qwen3 \ + --model_name "google/gemma-4-31B-it:cerebras" \ + --responses_api_base_url "https://router.huggingface.co/v1" \ + --responses_api_api_key "$HF_TOKEN" \ + --responses_api_reasoning_effort none \ + --responses_api_stream +``` + +### Fully Local + +Run the LLM in a separate llama.cpp process for the lowest-friction fully local setup, as shown in the [Reachy Mini local conversation guide](https://huggingface.co/blog/local-reachy-mini-conversation): + +```bash +# Terminal 1: llama.cpp serving Gemma 4 +llama-server -hf ggml-org/gemma-4-E4B-it-GGUF -np 2 -c 65536 -fa on --swa-full +``` + +```bash +# Terminal 2: speech-to-speech using that local LLM server +speech-to-speech \ + --mode realtime \ + --stt parakeet-tdt \ + --llm_backend responses-api \ + --tts qwen3 \ + --model_name "ggml-org/gemma-4-E4B-it-GGUF" \ + --responses_api_base_url "http://192.168.1.9:8080/v1" \ + --responses_api_api_key "" \ + --responses_api_stream \ + --enable_live_transcription +``` + +You can use `--mode local` instead of `--mode realtime` when you want to talk through the machine running the server directly. In-process local backends are still available with `--llm_backend mlx-lm` on Apple Silicon or `--llm_backend transformers` on CUDA / CPU. + +## Multi-Language Support + +Language coverage depends on the STT and TTS backends you pick, not on the pipeline itself: + +| Component | Backend | Languages | +|---|---|---| +| STT | Parakeet TDT (default) | 25 European languages | +| STT | Whisper / Whisper MLX / Faster Whisper | Broad multilingual coverage, depending on the selected Whisper checkpoint | +| STT | Paraformer | Depends on the selected FunASR checkpoint; the default is Chinese-oriented | +| TTS | Qwen3-TTS (default) | Multilingual, with `--qwen3_tts_language auto` by default | +| TTS | Kokoro | Multiple language/voice mappings, depending on backend availability | +| TTS | ChatTTS | English and Chinese | +| TTS | MMS TTS | Broad multilingual coverage through MMS checkpoints | + +Make sure the STT, LLM, and TTS you pair all cover your target language(s). Two usage patterns: + +- **Single language**: set `--language` to the target language code. The default is `en`. +- **Language switching**: set `--language auto`. The STT detects the language of each spoken prompt and forwards it to the LLM. Optionally add `--enable_lang_prompt` to append a "Please reply to my message in ..." instruction. It defaults to `False`; large LLMs usually infer the language from context, but the explicit instruction can help smaller models. + +Automatic language detection: + +```bash +speech-to-speech \ + --stt parakeet-tdt \ + --language auto \ + --llm_backend mlx-lm \ + --model_name "mlx-community/Qwen3-4B-Instruct-2507-bf16" +``` + +A single non-English language, Chinese in this example: + +```bash +speech-to-speech \ + --stt whisper-mlx \ + --stt_model_name large-v3 \ + --language zh \ + --llm_backend mlx-lm \ + --model_name mlx-community/Qwen3-4B-Instruct-2507-bf16 +``` + +Both commands also work on top of `--local_mac_optimal_settings`; explicit `--stt` flags override the defaults it sets. + +## Pocket TTS + +Pocket TTS from Kyutai Labs provides streaming TTS with voice cloning: + +```bash +speech-to-speech \ + --tts pocket \ + --pocket_tts_voice jean \ + --pocket_tts_device cpu +``` + +Available voice presets: `alba`, `marius`, `javert`, `jean`, `fantine`, `cosette`, `eponine`, `azelma`. Custom voice files and Hugging Face paths also work. + +## CLI Reference + +References for all CLI arguments live in the [arguments classes](./src/speech_to_speech/arguments_classes) and in `speech-to-speech -h`. + +### Module-Level Parameters + +See [ModuleArguments](./src/speech_to_speech/arguments_classes/module_arguments.py). It allows setting: + +- a common `--device`, if every part should run on the same device +- `--mode`: `realtime` (default), `local`, `socket`, or `websocket` +- STT implementation (`--stt`) +- LLM backend (`--llm_backend`: `transformers`, `mlx-lm`, `responses-api`, or `chat-completions`) +- TTS implementation (`--tts`) +- logging level +- realtime pipeline pool size (`--num_pipelines`) + +### VAD Parameters + +See [VADHandlerArguments](./src/speech_to_speech/arguments_classes/vad_arguments.py). Notable options: + +- `--thresh`: threshold value to trigger voice activity detection. +- `--min_speech_ms`: minimum duration of detected voice activity to be considered speech. +- `--min_speech_continuation_ms`: sustain-bar hysteresis threshold for speech that continues a reopenable soft-ended, uncommitted turn within the reopen window. The default and recommended pairing is `--min_speech_ms 384 --min_speech_continuation_ms 192`. +- `--min_silence_ms`: minimum length of silence intervals for segmenting speech. Default is 64 ms. +- `--short_segment_merge_ms`: optional merge window for stitching adjacent VAD segments that are each shorter than `--min_speech_ms`. +- `--unanswered_reopen_ms`: sanity cap on how long a soft-ended speculative turn that has not yet received any assistant output stays reopenable. + +### STT, LLM, and TTS Parameters + +`model_name`, `torch_dtype`, and `device` are exposed for each STT, LLM, and TTS implementation. STT and TTS parameters use the handler prefix, for example `--stt_model_name` or `--qwen3_tts_device`. LLM model selection and chat settings are shared across backends via unprefixed flags, for example `--model_name` and `--chat_size`; backend-specific flags use the `responses_api_` prefix for the `responses-api` and `chat-completions` backends and the `llm_` prefix for local backends. + +For example: + +```bash +# Local transformers/mlx-lm backend +--model_name google/gemma-2b-it + +# OpenAI-compatible backend +--llm_backend responses-api --model_name deepseek-chat --responses_api_base_url https://api.deepseek.com +``` + +### Generation Parameters + +Other generation parameters can be set using the handler prefix plus `_gen_`, for example `--stt_gen_max_new_tokens 128` or `--llm_gen_temperature 0.7`. Parameters not yet exposed can be added to the relevant arguments class. + +## Contributing + +Issues and PRs are welcome. Good starting points are the [open issues](https://github.com/huggingface/speech-to-speech/issues). For larger changes, open an issue first to discuss the approach. + +For local development: + +```bash +uv sync +pytest +ruff check +``` + +## Star History + +[![Star History Chart](assets/star-history.svg)](https://github.com/huggingface/speech-to-speech/stargazers) + +## Citations + +If you use this pipeline, please also cite the component models you run. The defaults are: + +### Silero VAD + +```bibtex +@misc{SileroVAD, + author = {Silero Team}, + title = {Silero VAD: pre-trained enterprise-grade Voice Activity Detector (VAD), Number Detector and Language Classifier}, + year = {2021}, + publisher = {GitHub}, + journal = {GitHub repository}, + howpublished = {\url{https://github.com/snakers4/silero-vad}}, + email = {hello@silero.ai} +} +``` + +### Parakeet TDT + +```bibtex +@misc{parakeet-tdt, + author = {NVIDIA}, + title = {Parakeet TDT 0.6B v3}, + publisher = {Hugging Face}, + howpublished = {\url{https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3}} +} +``` + +### Qwen3-TTS + +```bibtex +@misc{qwen3-tts, + author = {Qwen Team}, + title = {Qwen3-TTS}, + publisher = {Hugging Face}, + howpublished = {\url{https://huggingface.co/Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice}} +} +``` + +Citations for optional backends such as Kokoro, Pocket TTS, ChatTTS, Whisper variants, Paraformer, and MMS live in the respective [component READMEs](./src/speech_to_speech). diff --git a/backend/vendor/s2s-cloud/archive/README.md b/backend/vendor/s2s-cloud/archive/README.md new file mode 100644 index 0000000..86db6f6 --- /dev/null +++ b/backend/vendor/s2s-cloud/archive/README.md @@ -0,0 +1,11 @@ +# Archived Models + +This directory stores sunset model implementations that are kept in-repo but are no longer wired into `s2s_pipeline.py`. + +- STT: `moonshine` -> `archive/STT/moonshine_handler.py` +- TTS: `parler` -> `archive/TTS/parler_handler.py` +- TTS: `melo` -> `archive/TTS/melo_handler.py` +- Legacy args: `archive/arguments_classes/parler_tts_arguments.py` +- Legacy args: `archive/arguments_classes/melo_tts_arguments.py` + +These models are also removed from default requirements. If you want to run them manually, install their dependencies separately. diff --git a/backend/vendor/s2s-cloud/archive/STT/__init__.py b/backend/vendor/s2s-cloud/archive/STT/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/vendor/s2s-cloud/archive/STT/moonshine_handler.py b/backend/vendor/s2s-cloud/archive/STT/moonshine_handler.py new file mode 100644 index 0000000..ce6becd --- /dev/null +++ b/backend/vendor/s2s-cloud/archive/STT/moonshine_handler.py @@ -0,0 +1,72 @@ +import os + +os.environ['KERAS_BACKEND'] = 'torch' + +import logging + +import moonshine +import torch +from rich.console import Console + +from speech_to_speech.baseHandler import BaseHandler +from speech_to_speech.pipeline.messages import VADAudio + +logger = logging.getLogger(__name__) +console = Console() + + +class MoonshineSTTHandler(BaseHandler[VADAudio]): + """ + Handles the Speech To Text generation using a Moonshine model. + """ + + def setup( + self, + model_name="moonshine/base", + torch_dtype="float16", + gen_kwargs={}, + ): + self.torch_dtype = getattr(torch, torch_dtype) + self.gen_kwargs = gen_kwargs + + self.tokenizer = moonshine.load_tokenizer() + self.model = moonshine.load_model(model_name) + + self.warmup() + + def warmup(self): + logger.info(f"Warming up {self.__class__.__name__}") + + n_steps = 2 + dummy_input = torch.randn( + (1, 16000), + dtype=self.torch_dtype, + ) + + if torch.cuda.is_available(): + start_event = torch.cuda.Event(enable_timing=True) + end_event = torch.cuda.Event(enable_timing=True) + torch.cuda.synchronize() + start_event.record() + + for _ in range(n_steps): + _ = self.model.generate(dummy_input) + + if torch.cuda.is_available(): + end_event.record() + torch.cuda.synchronize() + + logger.info( + f"{self.__class__.__name__}: warmed up! time: {start_event.elapsed_time(end_event) * 1e-3:.3f} s" + ) + + def process(self, vad_audio: VADAudio): + logger.debug("infering moonshine...") + + pred_ids = self.model.generate(vad_audio.audio[None, :]) + pred_text = self.tokenizer.decode_batch(pred_ids)[0] + + logger.debug("finished whisper inference") + console.print(f"[yellow]USER: {pred_text}") + + yield (pred_text, "en") diff --git a/backend/vendor/s2s-cloud/archive/TTS/__init__.py b/backend/vendor/s2s-cloud/archive/TTS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/vendor/s2s-cloud/archive/TTS/melo_handler.py b/backend/vendor/s2s-cloud/archive/TTS/melo_handler.py new file mode 100644 index 0000000..6ac5d10 --- /dev/null +++ b/backend/vendor/s2s-cloud/archive/TTS/melo_handler.py @@ -0,0 +1,130 @@ +from __future__ import annotations + +import logging +from threading import Event +from typing import Any, Iterator + +import librosa +import numpy as np +import torch +from melo.api import TTS +from rich.console import Console + +from speech_to_speech.baseHandler import BaseHandler +from speech_to_speech.pipeline.cancel_scope import CancelScope +from speech_to_speech.pipeline.handler_types import TTSIn, TTSOut +from speech_to_speech.pipeline.messages import AUDIO_RESPONSE_DONE, EndOfResponse +from speech_to_speech.pipeline.speculative_turns import SpeculativeTurnTracker + +logger = logging.getLogger(__name__) + +console = Console() + +WHISPER_LANGUAGE_TO_MELO_LANGUAGE = { + "en": "EN", + "fr": "FR", + "es": "ES", + "zh": "ZH", + "ja": "JP", + "ko": "KR", +} + +WHISPER_LANGUAGE_TO_MELO_SPEAKER = { + "en": "EN-BR", + "fr": "FR", + "es": "ES", + "zh": "ZH", + "ja": "JP", + "ko": "KR", +} + + +class MeloTTSHandler(BaseHandler[TTSIn, TTSOut]): + def setup( + self, + should_listen: Event, + device: str = "mps", + language: str = "en", + speaker_to_id: str = "en", + gen_kwargs: dict[str, Any] = {}, # Unused + blocksize: int = 512, + cancel_scope: CancelScope | None = None, + speculative_turns: SpeculativeTurnTracker | None = None, + ) -> None: + self.should_listen = should_listen + self.cancel_scope = cancel_scope + self.speculative_turns = speculative_turns + self.device = device + self.language = language + self.model = TTS(language=WHISPER_LANGUAGE_TO_MELO_LANGUAGE[self.language], device=device) + self.speaker_id = self.model.hps.data.spk2id[WHISPER_LANGUAGE_TO_MELO_SPEAKER[speaker_to_id]] + self.blocksize = blocksize + self._initial_language = self.language + self.warmup() + + def warmup(self) -> None: + logger.info(f"Warming up {self.__class__.__name__}") + _ = self.model.tts_to_file("text", self.speaker_id, quiet=True) + + def process(self, tts_input: TTSIn) -> Iterator[TTSOut]: + if isinstance(tts_input, EndOfResponse): + yield AUDIO_RESPONSE_DONE + return + + speculative_turns = getattr(self, "speculative_turns", None) + if speculative_turns and not speculative_turns.is_latest( + tts_input.turn_id, + tts_input.turn_revision, + ): + logger.debug("Dropping stale TTS input for turn=%s rev=%s", tts_input.turn_id, tts_input.turn_revision) + return + + gen = self.cancel_scope.generation if self.cancel_scope else None + language_code = tts_input.language_code + text = tts_input.text + + console.print(f"[green]ASSISTANT: {text}") + + if language_code is not None and self.language != language_code: + try: + self.model = TTS( + language=WHISPER_LANGUAGE_TO_MELO_LANGUAGE[language_code], + device=self.device, + ) + self.speaker_id = self.model.hps.data.spk2id[WHISPER_LANGUAGE_TO_MELO_SPEAKER[language_code]] + self.language = language_code + except KeyError: + console.print(f"[red]Language {language_code} not supported by Melo. Using {self.language} instead.") + + if self.device == "mps": + import time + + start = time.time() + torch.mps.synchronize() # Waits for all kernels in all streams on the MPS device to complete. + torch.mps.empty_cache() # Frees all memory allocated by the MPS device. + _ = time.time() - start # Removing this line makes it fail more often. I'm looking into it. + + try: + audio_chunk = self.model.tts_to_file(text, self.speaker_id, quiet=True) + except (AssertionError, RuntimeError) as e: + logger.error(f"Error in MeloTTSHandler: {e}") + audio_chunk = np.array([]) + if len(audio_chunk) == 0: + return + audio_chunk = librosa.resample(audio_chunk, orig_sr=44100, target_sr=16000) + audio_chunk = (audio_chunk * 32768).astype(np.int16) + for i in range(0, len(audio_chunk), self.blocksize): + if gen is not None and self.cancel_scope is not None and self.cancel_scope.is_stale(gen): + logger.info("TTS generation cancelled (interruption)") + return + yield np.pad( + audio_chunk[i : i + self.blocksize], + (0, self.blocksize - len(audio_chunk[i : i + self.blocksize])), + ) + + def on_session_end(self) -> None: + if self.language != self._initial_language: + self.language = self._initial_language + self.model = TTS(language=WHISPER_LANGUAGE_TO_MELO_LANGUAGE[self.language], device=self.device) + self.speaker_id = self.model.hps.data.spk2id[WHISPER_LANGUAGE_TO_MELO_SPEAKER[self.language]] + logger.debug("Melo TTS session state reset") diff --git a/backend/vendor/s2s-cloud/archive/TTS/parler_handler.py b/backend/vendor/s2s-cloud/archive/TTS/parler_handler.py new file mode 100644 index 0000000..1580a76 --- /dev/null +++ b/backend/vendor/s2s-cloud/archive/TTS/parler_handler.py @@ -0,0 +1,244 @@ +from __future__ import annotations + +import logging +from threading import Event, Thread +from time import perf_counter +from typing import Any, Optional + +import librosa +import numpy as np +import torch +from parler_tts import ParlerTTSForConditionalGeneration, ParlerTTSStreamer +from rich.console import Console +from transformers import ( + AutoTokenizer, +) +from transformers.utils.import_utils import ( + is_flash_attn_2_available, +) + +from speech_to_speech.baseHandler import BaseHandler +from speech_to_speech.pipeline.messages import AUDIO_RESPONSE_DONE, EndOfResponse, TTSInput +from speech_to_speech.utils.utils import next_power_of_2 + +torch._inductor.config.fx_graph_cache = True +# mind about this parameter ! should be >= 2 * number of padded prompt sizes for TTS +torch._dynamo.config.cache_size_limit = 15 + +logger = logging.getLogger(__name__) + +console = Console() + + +if not is_flash_attn_2_available() and torch.cuda.is_available(): + logger.warn( + """Parler TTS works best with flash attention 2, but is not installed + Given that CUDA is available in this system, you can install flash attention 2 with `uv pip install flash-attn --no-build-isolation`""" + ) + + +WHISPER_LANGUAGE_TO_PARLER_SPEAKER = { + "en": "Jason", + "fr": "Christine", + "es": "Steven", + "de": "Nicole", + "pt": "Sophia", + "pl": "Alex", + "it": "Richard", + "nl": "Mark", +} + + +class ParlerTTSHandler(BaseHandler[TTSInput | EndOfResponse]): + def setup( + self, + should_listen, + model_name="parler-tts/parler-mini-v1-jenny", + device="cuda", + torch_dtype="float16", + compile_mode=None, + gen_kwargs={}, + max_prompt_pad_length=8, + description=( + "Jenny speaks at a slightly slow pace with an animated delivery with clear audio quality." + ), + play_steps_s=1, + blocksize=512, + use_default_speakers_list=True, + cancel_response: Event | None = None, + ): + self.should_listen = should_listen + self.cancel_response = cancel_response + self.device = device + self.torch_dtype = getattr(torch, torch_dtype) + self.gen_kwargs = gen_kwargs + self.compile_mode = compile_mode + self.max_prompt_pad_length = max_prompt_pad_length + self.use_default_speakers_list = use_default_speakers_list + if self.use_default_speakers_list: + description = description.replace("Jenny", "") + + self.speaker = "Jason" + self.description = description + + self.model = ParlerTTSForConditionalGeneration.from_pretrained( + model_name, torch_dtype=self.torch_dtype + ).to(device) + + self.description_tokenizer = AutoTokenizer.from_pretrained(self.model.config.text_encoder._name_or_path) + self.prompt_tokenizer = AutoTokenizer.from_pretrained(model_name) + + + framerate = self.model.audio_encoder.config.frame_rate + self.play_steps = int(framerate * play_steps_s) + self.blocksize = blocksize + + if self.compile_mode not in (None, "default"): + logger.warning( + "Torch compilation modes that captures CUDA graphs are not yet compatible with the TTS part. Reverting to 'default'" + ) + self.compile_mode = "default" + + if self.compile_mode: + self.model.generation_config.cache_implementation = "static" + self.model.forward = torch.compile( + self.model.forward, mode=self.compile_mode, fullgraph=True + ) + + self.warmup() + + def prepare_model_inputs( + self, + prompt, + max_length_prompt=50, + pad=False, + ): + pad_args_prompt = ( + {"padding": "max_length", "max_length": max_length_prompt} if pad else {} + ) + + description = self.description + if self.use_default_speakers_list: + description = self.speaker + " " + self.description + + tokenized_description = self.description_tokenizer( + description, return_tensors="pt" + ).to(self.device) + input_ids = tokenized_description.input_ids + attention_mask = tokenized_description.attention_mask + + tokenized_prompt = self.prompt_tokenizer( + prompt, return_tensors="pt", **pad_args_prompt + ).to(self.device) + prompt_input_ids = tokenized_prompt.input_ids + prompt_attention_mask = tokenized_prompt.attention_mask + + gen_kwargs = { + "input_ids": input_ids, + "attention_mask": attention_mask, + "prompt_input_ids": prompt_input_ids, + "prompt_attention_mask": prompt_attention_mask, + **self.gen_kwargs, + } + + return gen_kwargs + + def warmup(self): + logger.info(f"Warming up {self.__class__.__name__}") + + if self.device == "cuda": + start_event = torch.cuda.Event(enable_timing=True) + end_event = torch.cuda.Event(enable_timing=True) + + # 2 warmup steps for no compile or compile mode with CUDA graphs capture + n_steps = 1 if self.compile_mode == "default" else 2 + + if self.device == "cuda": + torch.cuda.synchronize() + start_event.record() + if self.compile_mode: + pad_lengths = [2**i for i in range(2, self.max_prompt_pad_length)] + for pad_length in pad_lengths[::-1]: + model_kwargs = self.prepare_model_inputs( + "dummy prompt", max_length_prompt=pad_length, pad=True + ) + for _ in range(n_steps): + _ = self.model.generate(**model_kwargs) + logger.info(f"Warmed up length {pad_length} tokens!") + else: + model_kwargs = self.prepare_model_inputs("dummy prompt") + for _ in range(n_steps): + _ = self.model.generate(**model_kwargs) + + if self.device == "cuda": + end_event.record() + torch.cuda.synchronize() + logger.info( + f"{self.__class__.__name__}: warmed up! time: {start_event.elapsed_time(end_event) * 1e-3:.3f} s" + ) + + def process(self, tts_input: TTSInput | EndOfResponse): + if isinstance(tts_input, EndOfResponse): + yield AUDIO_RESPONSE_DONE + return + + runtime_config = tts_input.runtime_config + response = tts_input.response + language_code = tts_input.language_code + text = tts_input.text + + voice: Optional[str] = None + if response and response.audio and response.audio.output: + voice = str(response.audio.output.voice) if response.audio.output.voice is not None else None + if not voice and runtime_config: + audio_cfg = runtime_config.session.audio + audio_output = audio_cfg.output if audio_cfg is not None else None + voice = str(audio_output.voice) if audio_output is not None and audio_output.voice else None + if voice: + self.speaker = voice + elif language_code: + self.speaker = WHISPER_LANGUAGE_TO_PARLER_SPEAKER.get(language_code, "Jason") + + console.print(f"[green]ASSISTANT: {text}") + nb_tokens = len(self.prompt_tokenizer(text).input_ids) + + pad_args: dict[str, Any] = {} + if self.compile_mode: + # pad to closest upper power of two + pad_length = next_power_of_2(nb_tokens) + logger.debug(f"padding to {pad_length}") + pad_args["pad"] = True + pad_args["max_length_prompt"] = pad_length + + tts_gen_kwargs = self.prepare_model_inputs( + text, + **pad_args, + ) + + streamer = ParlerTTSStreamer( + self.model, device=self.device, play_steps=self.play_steps + ) + tts_gen_kwargs = {"streamer": streamer, **tts_gen_kwargs} + torch.manual_seed(0) + thread = Thread(target=self.model.generate, kwargs=tts_gen_kwargs) + thread.start() + + pipeline_start = perf_counter() + for i, audio_chunk in enumerate(streamer): + if self.cancel_response and self.cancel_response.is_set(): + logger.info("TTS generation cancelled (interruption)") + return + if i == 0: + logger.info( + f"Time to first audio: {perf_counter() - pipeline_start:.3f}s" + ) + audio_chunk = librosa.resample(audio_chunk, orig_sr=44100, target_sr=16000) + audio_chunk = (audio_chunk * 32768).astype(np.int16) + for i in range(0, len(audio_chunk), self.blocksize): + yield np.pad( + audio_chunk[i : i + self.blocksize], + (0, self.blocksize - len(audio_chunk[i : i + self.blocksize])), + ) + + if not runtime_config: + self.should_listen.set() diff --git a/backend/vendor/s2s-cloud/archive/__init__.py b/backend/vendor/s2s-cloud/archive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/vendor/s2s-cloud/archive/arguments_classes/__init__.py b/backend/vendor/s2s-cloud/archive/arguments_classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/vendor/s2s-cloud/archive/arguments_classes/melo_tts_arguments.py b/backend/vendor/s2s-cloud/archive/arguments_classes/melo_tts_arguments.py new file mode 100644 index 0000000..283045b --- /dev/null +++ b/backend/vendor/s2s-cloud/archive/arguments_classes/melo_tts_arguments.py @@ -0,0 +1,17 @@ +from dataclasses import dataclass, field + + +@dataclass +class MeloTTSHandlerArguments: + melo_language: str = field( + default="en", + metadata={"help": "The language of the text to be synthesized. Default is 'EN_NEWEST'."}, + ) + melo_device: str = field( + default="auto", + metadata={"help": "The device to be used for speech synthesis. Default is 'auto'."}, + ) + melo_speaker_to_id: str = field( + default="en", + metadata={"help": "Mapping of speaker names to speaker IDs. Default is ['EN-Newest']."}, + ) diff --git a/backend/vendor/s2s-cloud/archive/arguments_classes/parler_tts_arguments.py b/backend/vendor/s2s-cloud/archive/arguments_classes/parler_tts_arguments.py new file mode 100644 index 0000000..eea37cb --- /dev/null +++ b/backend/vendor/s2s-cloud/archive/arguments_classes/parler_tts_arguments.py @@ -0,0 +1,68 @@ +from dataclasses import dataclass, field +from typing import Optional + + +@dataclass +class ParlerTTSHandlerArguments: + tts_model_name: str = field( + default="parler-tts/parler-mini-v1-jenny", + metadata={ + "help": "The pretrained TTS model to use. Default is 'parler-tts/parler-mini-v1-jenny'." + }, + ) + tts_device: str = field( + default="cuda", + metadata={ + "help": "The device type on which the model will run. Default is 'cuda' for GPU acceleration." + }, + ) + tts_torch_dtype: str = field( + default="float16", + metadata={ + "help": "The PyTorch data type for the model and input tensors. One of `float32` (full-precision), `float16` or `bfloat16` (both half-precision)." + }, + ) + tts_compile_mode: Optional[str] = field( + default=None, + metadata={ + "help": "Compile mode for torch compile. Either 'default', 'reduce-overhead' and 'max-autotune'. Default is None (no compilation)" + }, + ) + tts_gen_min_new_tokens: int = field( + default=64, + metadata={ + "help": "Maximum number of new tokens to generate in a single completion. Default is 64, which corresponds to ~0.74 secs" + }, + ) + tts_gen_max_new_tokens: int = field( + default=1024, + metadata={ + "help": "Maximum number of new tokens to generate in a single completion. Default is 1024, which corresponds to ~12 secs" + }, + ) + description: str = field( + default=( + "Jenny speaks at a slightly slow pace with an animated delivery with clear audio quality." + ), + metadata={ + "help": "Description of the speaker's voice and speaking style to guide the TTS model." + }, + ) + play_steps_s: float = field( + default=1.0, + metadata={ + "help": "The time interval in seconds for playing back the generated speech in steps. Default is 1.0 seconds." + }, + ) + max_prompt_pad_length: int = field( + default=8, + metadata={ + "help": "When using compilation, the prompt as to be padded to closest power of 2. This parameters sets the maximun power of 2 possible." + }, + ) + use_default_speakers_list: bool = field( + default=False, + metadata={ + "help": "Whether to use the default list of speakers or not." + }, + ) diff --git a/backend/vendor/s2s-cloud/assets/star-history.svg b/backend/vendor/s2s-cloud/assets/star-history.svg new file mode 100644 index 0000000..06ce698 --- /dev/null +++ b/backend/vendor/s2s-cloud/assets/star-history.svg @@ -0,0 +1,15 @@ + + + huggingface/speech-to-speech star history + 6,165 stars + + 01.6k3.2k4.8k6.4k8k + Aug 2024Jan 2025May 2025Oct 2025Feb 2026Jul 2026 + + + + diff --git a/backend/vendor/s2s-cloud/demo/.dockerignore b/backend/vendor/s2s-cloud/demo/.dockerignore new file mode 100644 index 0000000..88b4b68 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitignore +__pycache__/ +*.pyc +.venv/ +venv/ +.vscode/ +.idea/ +.DS_Store +*.log +docs/ +.claude/ diff --git a/backend/vendor/s2s-cloud/demo/.gitignore b/backend/vendor/s2s-cloud/demo/.gitignore new file mode 100644 index 0000000..f43f76b --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/.gitignore @@ -0,0 +1,21 @@ +# macOS metadata +.DS_Store + +# Editor scratch +.vscode/ +.idea/ +*.swp + +# Static HTTP server logs (we don't ship them) +*.log + +# Python +__pycache__/ +*.pyc +.venv/ +venv/ + +# Local-only replica deploy script (never tracked) +deploy_replica.py + +.env \ No newline at end of file diff --git a/backend/vendor/s2s-cloud/demo/CONTEXT.md b/backend/vendor/s2s-cloud/demo/CONTEXT.md new file mode 100644 index 0000000..b4d980a --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/CONTEXT.md @@ -0,0 +1,108 @@ +# Context glossary + +Canonical terms for this space. A glossary, not a spec — it defines what words mean, +not how anything is built. Keep design/implementation detail in `DESIGN.md` and the +code. + +## Speech-to-speech demo +The product: a voice conversation you have with a model by tapping the orb and +talking. "The demo" and "the space" refer to this same thing. It runs on Hugging +Face's open `speech-to-speech` backend. + +## The pipeline +The ordered path a turn travels, from your voice to the orb's reply. Order is +meaningful — each stage consumes the previous one's output: + +`you speak → VAD → STT → VLM → TTS → orb replies` + +- **VAD** — voice activity detection. Decides *when* you are speaking, so the system + knows a turn has started and ended. Model: silero-vad. +- **STT** — speech to text. Transcribes your speech into words. Model: + nvidia/parakeet-tdt-1.1b. +- **VLM** — the vision-language model that composes the reply. Served via Cerebras. + Model: google/gemma-4-31B-it. +- **TTS** — text to speech. Speaks the reply back in the chosen voice. Model: + Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice. + +## Builder +A Hugging Face user credited with making the space, shown by HF username. Current +builders: tfrere, A-Mahla and andito. Distinct from the *models'* authors (nvidia, google, +Qwen, snakers4), who are credited per pipeline stage. + +## Powered by +The infrastructure running the pipeline, named in the about panel: Hugging Face +Inference Endpoints (hosting) and Cerebras (LLM inference). Distinct from "built by" +(the people) and from the model authors (who trained each model). + +## Tool +A function the model can call mid-conversation to do something the pipeline +can't do on its own (look something up, look through the camera). Tools are +declared to the backend in the session config; the model decides when to call +one. Distinct from the *pipeline stages* (VAD/STT/VLM/TTS), which always run. + +## Tool executor +The client-side component that runs a tool when the model calls it and returns +the result to the backend, so the model can speak the answer. It is the missing +half of the round-trip: the backend already emits the call, the executor runs it +and replies. Distinct from the *tool* itself (the thing being run). + +## Web search tool +A tool that looks something up on the web for the model. The model calls it with +a query; the tool executor forwards the query to the search proxy and returns the +results as the tool result. Activates only when a search key is available. + +## Camera snapshot tool +A tool that lets the model look through the user's webcam. While enabled, a live +self-view is shown in the page (bottom-left); when the model calls the tool, the +executor captures a frame and sends it to the model as an image so the VLM can +see it. Distinct from the *preview* (what the user sees) and the *snapshot* (the +single frame sent to the model). + +## Search proxy +The same-origin server route (`/search`) that holds the search key and calls the +external search provider on the client's behalf, so the key never reaches the +browser. Lives in the same container as the page. Distinct from the *s2s backend* +(the separate load-balanced speech-to-speech service). + +## Tools panel +The dialog opened from the "Tools" button in the top-right, holding one switch per +tool (and the web-search key status). Turning a switch on/off declares or removes +that tool on the live session. Distinct from *Settings* (connection, voice, +instructions) and the *About panel* (project info). + +## Identity block +The top-left corner of the topbar (replacing the old wordmark): the demo name, a +one-line blurb, and the "powered by" / "built by" credits, shown directly rather +than hidden behind a click. + +## About panel +The popup opened from the (i) icon to the right of the identity block. Holds the +general introduction to the speech-to-speech project (with a repo link) and the +pipeline. Identity itself now lives in the corner, not here. + +## Queue +The line of users waiting for a free conversation slot when every compute is +busy. You join the queue instead of being turned away; you leave it by reaching +the front or by giving up. Distinct from a *session* (an actual live +conversation) — being in the queue is not yet talking, and time spent waiting +never counts against your usage limit. + +## Ticket +Your held place in the queue. Created when you join, it is what the demo checks +to tell you your position and to notice if you have left. A ticket is not a +session: it only promises a spot in line, not a compute. + +## Position +How many people are ahead of you in the queue, shown while you wait ("You're #3 +in line"). It only ever counts down. Distinct from an *estimated wait* — the +demo shows position, never a time, because wait time is unpredictable. + +## Claim +The moment you reach the front and a free slot becomes yours — the queue hands +off to a real session and the conversation begins. This is also the point where +your usage limit first starts to matter (never while waiting). + +## At capacity +The state where the queue itself is full, so new users can't even join the line +and are asked to try again shortly. Distinct from simply *busy* (all computes +taken but the queue still has room to wait in). diff --git a/backend/vendor/s2s-cloud/demo/DESIGN.md b/backend/vendor/s2s-cloud/demo/DESIGN.md new file mode 100644 index 0000000..d0ca548 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/DESIGN.md @@ -0,0 +1,205 @@ +# Design language + +The reference for keeping this app visually coherent as it grows. Read it before +touching `style.css`, `index.html`, or any DOM-building code in `main.js`. Every +rule here is already live in the codebase — this file explains the *why* so changes +extend the system instead of drifting from it. + +--- + +## The thesis: color belongs to the voice + +This is a voice app. The one thing in the room that should have color is the thing +that is talking. So: + +- **The orb** carries saturated color. It glows, and the glow's hue changes with + conversational state. +- **Everything else is monochrome** — a precise cool-grey dark canvas. Surfaces, + borders, buttons, panels, the transcript: all greyscale. +- The **only** exceptions are tiny *role echoes* (a one-word mono label, a small + icon) that borrow the orb's state hue so the transcript reads in the same color + language the orb speaks. They are accents the size of a word, never fills. +- **Brand logos keep their own color.** The Hugging Face mark (`#ffd21e`) and the + Cerebras mark (`#f15a29`) render in their brand colors in the identity credits and + the about panel — a deliberate, owner-approved exception. It applies to those two + logos only; do not generalize it to other chrome. + +If you find yourself adding a tinted background, a colored border, or a bright +button anywhere outside the orb, stop — that color almost certainly belongs to the +orb instead, or shouldn't exist. + +--- + +## Color tokens + +All defined in `:root` in `style.css`. Use the variables, never raw hex in rules. + +### Canvas (the monochrome world) +| Token | Value | Use | +|---|---|---| +| `--bg` | `#0a0b10` | Page background (a cool near-black) | +| `--bg-elev` | `#13151c` | Raised surfaces: bubbles, panels, icon buttons | +| `--bg-elev-2` | `#1b1e29` | Surfaces on surfaces: history bodies, inputs | +| `--border` | `rgba(255,255,255,.08)` | Default hairline | +| `--border-strong` | `rgba(255,255,255,.16)` | Emphasised hairline | +| `--text` | `#f5f6fa` | Primary text; also the *primary button* fill | +| `--text-dim` | `rgba(245,246,250,.65)` | Secondary text | +| `--text-faint` | `rgba(245,246,250,.42)` | Captions, labels, footer | + +### Voice (the only saturated hues) +These are the orb's state colors. They appear on the orb, and as small role echoes +in the transcript — nowhere else. + +| Token | Value | Meaning | +|---|---|---| +| `--accent` / `--speaking` | `#8b7dff` violet | Assistant speaking | +| `--accent-2` / `--listening` | `#22d3ee` cyan | You / listening | +| `--processing` | `#f59e0b` amber | Thinking / tool call | +| `--error` | `#ff6a75` | Error | +| `--success` | `#34d399` | Ready / connected | + +### Role echoes (semantic aliases — use these in chat code) +| Token | Maps to | Where it shows | +|---|---|---| +| `--voice-user` | cyan | `YOU` label + user bubble accents | +| `--voice-assistant` | violet | `ASSISTANT` label + assistant accents | +| `--voice-tool` | amber | `TOOL CALL` label, wrench icon | + +**Why this mapping:** it mirrors the orb exactly — when *you* speak the orb is cyan +(`state-listening`), when the *assistant* speaks it's violet (`state-ai-speaking`), +when it's working it's amber (`state-processing`). The transcript is a quiet replay +of the orb's color story. + +### Orb state → glow (`.circle.state-*` → `--glow`) +| State | Glow | +|---|---| +| `signed-out` | violet `#8b7dff` | +| `authenticated` / `ready` | green `#34d399` | +| `connecting` / `connected` / `starting` | yellow `#facc15` | +| `listening` / `user-speaking` | cyan (`--listening`) | +| `processing` | amber (`--processing`) | +| `ai-speaking` | violet (`--speaking`) | +| `error` | red (`--error`) | + +Adding a new state? Give it a `--glow`, and if it surfaces in the transcript, add a +matching `--voice-*` alias rather than a one-off color. + +--- + +## Typography + +Two faces, two jobs. Never reach for a third. + +- **Inter** — body and UI. Wordmark, buttons, inputs, panel titles, prose, history + message bodies. The workhorse; it should feel neutral and get out of the way. +- **Geist Mono** (`--font-mono`) — the **machine voice**. Reserved for text the + *system* emits or identifiers it reports, never for human prose. + +### When mono is correct +Mono signals "this is the machine talking or naming itself." Use it for: +- the orb's status caption (`.circle-caption`) +- role eyebrows (`YOU` / `ASSISTANT` / `TOOL CALL`) +- tool-call names and argument JSON +- the `·WebSocket` transport tag, bitrate readouts, connection identifiers +- the empty-state label + +Mono text is set uppercase with `letter-spacing: ~0.1–0.14em` and weight `500`, so it +reads as a typed status line, not a headline. Body copy, button labels, and +explanatory `small` text stay **Inter** — putting prose in mono breaks the metaphor. + +The font is loaded in `index.html`; the stack falls back to system mono gracefully if +the CDN is blocked. + +--- + +## Layout + +- **One continuous canvas.** No dividers under the topbar or above the footer, no + panel chrome competing with content. The topbar and footer float over the stage. + Keep it that way — a new section earns a hairline (`--border`) only if it genuinely + needs separating. +- **The orb is the hero and the center of gravity.** It sits dead-center on the + stage. Controls flank it (mic / stop), captions sit beneath. Don't crowd it. +- **Hairlines, not boxes.** Separation comes from `1px` borders at 8–16% white and + from spacing, not from heavy fills or shadows. Shadows are soft and low + (`0 4px 18px rgba(0,0,0,.32)`), used only to lift floating elements (bubbles, + panels, modal). +- **Radii:** `--radius-sm: 8px` (buttons, inputs, chips), `--radius-md: 14px` + (bubbles, message bodies, modal), `--radius-lg: 22px` (reserved). Pick by element + size; don't invent new values. +- **Two reading surfaces for the transcript:** ephemeral bubbles top-right (desktop + only) that log and fade, and a slide-in history panel for review. On phones the + bubble stream is dropped and the panel goes full-screen — the panel is the single + source of truth there. + +--- + +## Components + +- **Buttons.** Default (`.btn`) is a neutral elevated surface. The *primary* button + is **near-white on dark** (`--text` fill, `--bg` text) — the highest-contrast thing + on the page that *isn't* the orb. There is no colored button; emphasis comes from + contrast, not hue. +- **Icon buttons** (`.icon-btn`) are `36px`, elevated surface, dim icon that brightens + on hover. Side controls (`.side-btn`) are circular, collapse to zero size until the + session is live (by width on desktop, by height in the mobile column). +- **Chat bubbles & history messages** share one neutral surface. They are + distinguished by **side** (you = left, assistant = right) plus the **mono role + label** in the role-echo hue — not by tinted fills. Tool entries use the wrench icon + + mono + amber, on the same neutral surface. +- **Badge** (new-message dot) is monochrome white — a signal, not a color accent. +- **Focus** is visible and neutral: inputs focus to `--text-dim`; the orb uses a + `--glow`-colored outline (it's the orb, so color is allowed). + +--- + +## Motion + +- **The orb is audio-reactive, not timer-driven.** Mic RMS (`--audio-level`) and the + assistant output level (`--ai-audio-level`) drive scale/opacity at display rate, so + every syllable moves it. This is the signature animation — keep new motion + subordinate to it. +- **Quiet by default.** Breathing/glow throbs are slow (1.4–2.4s) and low-contrast. + Resist adding scattered micro-animations; an orchestrated moment beats many small + ones, and excess motion reads as AI-generated. +- **First paint is frozen.** `body.booting` disables all transitions until the first + frame commits (stripped after one rAF in `main.js`). Anything new that would + otherwise animate-in on load must respect this. +- Honor `prefers-reduced-motion` for any motion you add. + +--- + +## Writing / copy voice + +- Sentence case, plain verbs, no filler. Tuned and quiet — match the minimal canvas. +- Name things by what the user controls, not by the system's internals. A button says + exactly what it does, and keeps the same word through the flow. +- **Empty states invite action** ("Tap the orb and start talking"), they don't just + set a mood. +- **Errors state what happened and how to recover**, in the interface's voice — they + don't apologize and are never vague. +- Mono labels are terse identifiers (`YOU`, `TOOL CALL`); prose stays in Inter. + +--- + +## Responsive floor (non-negotiable) + +Every change ships meeting these: +- Works down to a `360px`-wide phone. The `@media (max-width: 600px)` block already + handles the phone layout — extend it, don't fight it. +- Visible keyboard focus on every interactive element. +- `prefers-reduced-motion` respected. +- Tap targets ≥ `44px`; `touch-action: manipulation` on anything tappable. + +--- + +## Before you ship — the mirror check + +1. Is every saturated color either on the orb or a word-sized role echo? If a fill or + border is colored, remove that accessory. +2. Is mono used only for machine/system text, and Inter for everything human? +3. Does any new state have both a `--glow` and (if it appears in chat) a `--voice-*` + alias? +4. Are separations hairlines + spacing, not boxes and heavy shadows? +5. Did you add motion? Is it quieter than the orb and reduced-motion-safe? +6. Remove one accessory. The minimal look survives on precision, not addition. diff --git a/backend/vendor/s2s-cloud/demo/Dockerfile b/backend/vendor/s2s-cloud/demo/Dockerfile new file mode 100644 index 0000000..5d3e074 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/Dockerfile @@ -0,0 +1,14 @@ +# Single container: serves the static front-end AND the /api/search proxy. +# HF Spaces (sdk: docker) routes traffic to $PORT, default 7860. +FROM python:3.11-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 7860 + +CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"] diff --git a/backend/vendor/s2s-cloud/demo/README.md b/backend/vendor/s2s-cloud/demo/README.md new file mode 100644 index 0000000..6bd767a --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/README.md @@ -0,0 +1,190 @@ +--- +title: HF Realtime Voice +emoji: 🎙️ +colorFrom: indigo +colorTo: purple +sdk: docker +app_port: 7860 +pinned: false +short_description: Voice chat over WebSocket against a HF speech-to-speech +hf_oauth: true +--- + +# Realtime Voice Demo (WebSocket transport) + +Browser voice-chat UI for the +[huggingface/speech-to-speech](https://github.com/huggingface/speech-to-speech) +backend. The browser streams mic audio over a WebSocket using the OpenAI +Realtime **GA** protocol and plays back the assistant's audio as it arrives. + +## Quick start (local) + +1. **Start the speech-to-speech backend** in realtime mode (from the repo root; + see the [backend README](https://github.com/huggingface/speech-to-speech/blob/main/src/speech_to_speech/api/openai_realtime/README.md) + for more model combinations): + + ```bash + uv run speech-to-speech \ + --mode realtime \ + --stt parakeet-tdt \ + --llm_backend transformers \ + --tts kokoro \ + --model_name "Qwen/Qwen3-4B-Instruct-2507" \ + --llm_device mps \ + --llm_torch_dtype float16 \ + --enable_live_transcription + ``` + + The realtime server listens on `ws://localhost:8765/v1/realtime` by default + (`--ws_host` / `--ws_port` to change). + +2. **Start this app**, pointing it at the backend with `SPEECH_TO_SPEECH_URL`: + + ```bash + uv pip install -r demo/requirements.txt + export SPEECH_TO_SPEECH_URL=ws://localhost:8765/v1/realtime + export SERPER_API_KEY=... # optional; web search is disabled without it + uv run uvicorn --app-dir demo server:app --reload --port 7860 + ``` + + Or with Docker: + + ```bash + docker build -t s2s-demo demo/ + docker run -p 7860:7860 -e SPEECH_TO_SPEECH_URL=ws://host.docker.internal:8765/v1/realtime s2s-demo + ``` + +3. Open , click the orb, allow the mic, talk. + +> Browsers require **HTTPS or `localhost`** for `getUserMedia()` (mic + camera). +> `192.168.1.9` and `localhost` both work; plain `http://192.168.x.y` does NOT. + +Smoke-test the backend from the shell: + +```bash +websocat ws://localhost:8765/v1/realtime +# -> you should get a session.created event back immediately +``` + +## How it works + +1. The browser opens a WebSocket on the configured `/v1/realtime` URL. +2. Server pushes `session.created` on connect. Client replies with + `session.update` (OpenAI Realtime **GA** schema: `session.audio.input`, + `session.audio.output`, `session.output_modalities`). +3. Client streams mic audio as PCM16 16 kHz mono base64 chunks + (`input_audio_buffer.append`, one frame every ~40 ms). +4. Server pushes `response.output_audio.delta` (PCM16 24 kHz mono base64) + and transcript deltas. + +The backend exposes one concurrent session per pipeline unit +(`--num_pipelines` to serve more). + +## Connecting to a backend + +Three modes, picked by env (`/api/config` tells the client which one is active): + +- **`SPEECH_TO_SPEECH_URL` env** — the mode you want for local use, and the + highest priority. The browser connects **directly** to this realtime + WebSocket URL; it's shown read-only in Settings. Setting it disables the + load-balancer logic entirely (no `/api/session` proxy, no queue, no + metering, no sign-in). Unlike the LB address it is not a secret. Accepts a + full `ws(s)://host/v1/realtime` URL or a bare host like `localhost:8765` + (the app adds `/v1/realtime`). +- **Neither env set** — **Settings → Speech-to-speech server URL**: paste a + full connect URL or a bare host, and the browser connects to it directly. +- **`LOAD_BALANCER_URL` env** — multi-compute deployments only: the browser + POSTs the same-origin `/api/session` proxy, the server forwards to the LB, + and the browser dials the per-session compute URL the LB hands back. The LB + address never reaches the browser; the Settings URL field is hidden. + +| `SPEECH_TO_SPEECH_URL` | `LOAD_BALANCER_URL` | `SPACE_ID` | Connection | URL field | Metering | +|:---:|:---:|:---:|---|---|---| +| ✅ | any | any | direct → pinned URL | visible, locked | off | +| – | – | any | direct → user URL | editable | off | +| – | ✅ | ✅ | LB proxy | hidden | **on** | +| – | ✅ | – | LB proxy | hidden | off | + +**Settings → Restart** reconnects with the current voice, instructions and URL. + +## Tools + +The assistant can call two tools mid-conversation (toggle them from the **Tools** +button, top-right): + +- **Web search** — Google results via Serper.dev, proxied server-side so the key + never reaches the browser. Set `SERPER_API_KEY` as an env var / Space secret. + Without it, the tool is disabled unless the user pastes their own key in the + Tools panel. +- **Camera** — while enabled, a live self-view shows bottom-left; when the model + calls the tool, the current frame is sent to the vision-language model so it can + see what you're showing it. + +## Usage limits (deployed Space only) + +Conversation time is metered per UTC day by sign-in tier (see `limiter.py` / +`auth.py`), but **only on the deployed Space** — metering turns on only when BOTH +`LOAD_BALANCER_URL` and `SPACE_ID` (injected automatically by the HF Space +runtime) are present. Running locally — even with `LOAD_BALANCER_URL` exported — +leaves the app unmetered. Tunable via env: + +| Env | Default | What | +|-----|---------|------| +| `LIMIT_ANON_SEC` | `300` | Daily seconds for anonymous visitors (5 min) | +| `LIMIT_FREE_SEC` | `600` | Daily seconds for signed-in non-PRO users (10 min) | +| `UNLIMITED_ORGS` | _(adds to defaults)_ | Extra HF org names whose members get **unlimited** usage, like PRO | +| `USAGE_HASH_SECRET` | _(random)_ | HMAC secret for hashing identity keys + signing the anon cookie | + +PRO members are always unlimited. Members of `cerebras`, `HuggingFaceM4`, +`smolagents`, and `pollen-robotics` are unlimited out of the box (shown as +"Team", not "PRO"); set `UNLIMITED_ORGS=my-team` to add more. Matched +case-insensitively against the user's organisations from HF OAuth. + +## Settings (stored in `localStorage`) + +| Key | What | +|-----|------| +| Speech-to-speech server URL | Direct realtime WebSocket URL (hidden/locked when pinned by env) | +| Voice | Qwen3-TTS speaker name (Aiden, Ryan, Dylan, Eric, Ono_Anna, Serena, Sohee, Uncle_Fu, Vivian) | +| Instructions | System prompt sent in `session.update` once the WS opens | + +LocalStorage keys are namespaced `s2s.ws.*` so this app's settings do +NOT collide with the WebRTC variant. + +## Files + +| File | Role | +|------|------| +| `index.html` | Single page, orb + settings modal (identical UI to the WebRTC app) | +| `main.js` | State machine, settings, tools, camera, noise-gate UI wiring | +| `ui/chat.js` | `ChatView`: history panel, ephemeral bubbles, transcript/tool streaming | +| `ui/account.js` | `Account`: HF login chip + popover, daily-limit modal | +| `ui/dom.js` | Shared helpers: `$`, `escHtml`, `truncateError`, `DEBUG` | +| `auth.py` | HF OAuth + per-request identity (tier, hashed keys) | +| `limiter.py` | SQLite per-day talk-time budget (chunked server-clock reservation) | +| `ws/s2s-ws-client.js` | WebSocket handshake + OpenAI Realtime GA protocol | +| `ws/codec.js` | base64 <-> PCM helpers + transcript extraction (pure) | +| `ws/orb-visualizer.js` | `OrbVisualiser`: FFT bands -> orb CSS custom properties | +| `worklets/mic-capture.js` | AudioWorklet: 48 kHz Float32 -> 16 kHz Int16 PCM, posts ~40 ms chunks | +| `worklets/audio-playback.js` | AudioWorklet: 24 kHz Float32 ring buffer -> 48 kHz, linear interp, fade in/out | +| `style.css` | Orb animations, layout, dark theme (verbatim from the WebRTC app) | + +## Audio pipeline notes + +- **Input**: `getUserMedia({ echoCancellation, noiseSuppression, autoGainControl })` + feeds the `mic-capture` worklet at the `AudioContext` rate. The worklet + resamples to 16 kHz (boxcar lowpass + decimation on the 48 -> 16 fast + path, linear interpolation fallback for odd rates) and packs Int16 LE. +- **Output**: `response.output_audio.delta` decodes to Int16 -> Float32 + and is posted to the `audio-playback` worklet. The worklet maintains a + per-context ring buffer, linearly interpolates 24 -> 48, and applies + short 32-frame fades on entry/exit to suppress clicks. +- **Barge-in**: when the server VAD detects user speech mid-response + (`input_audio_buffer.speech_started` while `ai-speaking`), the client + posts `{ kind: "clear" }` to the playback worklet to wipe the queue + immediately. The server itself cancels the in-flight response. + +## Credits + +- Backend: [huggingface/speech-to-speech](https://github.com/huggingface/speech-to-speech) +- UI verbatim from `amir-tfrere/minimal-conversation-app-s2s-backend` (Pollen Robotics × Hugging Face) diff --git a/backend/vendor/s2s-cloud/demo/__init__.py b/backend/vendor/s2s-cloud/demo/__init__.py new file mode 100644 index 0000000..b1c139d --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/__init__.py @@ -0,0 +1 @@ +# demo as package (required by uvicorn demo.server:app) diff --git a/backend/vendor/s2s-cloud/demo/auth.py b/backend/vendor/s2s-cloud/demo/auth.py new file mode 100644 index 0000000..4ede932 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/auth.py @@ -0,0 +1,229 @@ +""" +HF OAuth + per-request identity for the duration limiter. + +Login uses Hugging Face's native Spaces OAuth via `huggingface_hub` +(`attach_huggingface_oauth` / `parse_huggingface_oauth`). The OAuth env +(`OAUTH_CLIENT_ID`, ...) is injected by the platform when the Space README sets +`hf_oauth: true`, so this only activates on a deployed Space — locally and in +direct mode there's no OAuth and the limiter treats everyone as anonymous. + +Identity: + - signed in -> tier 'pro' | 'free', keyed by hashed HF `sub` + - anonymous -> tier 'anon', keyed by BOTH hashed client IP and a hashed + signed-cookie id (OR-matched in the limiter) +""" + +import logging +import os +import secrets + +from . import limiter + +logger = logging.getLogger("s2s.auth") + +# huggingface_hub adds these routes when OAuth is attached. Centralised so a +# version change is a one-line fix; the paths are handed to the client via +# /api/me rather than hardcoded there. +OAUTH_LOGIN_PATH = "/oauth/huggingface/login" +OAUTH_LOGOUT_PATH = "/oauth/huggingface/logout" + +ANON_COOKIE = "s2s_anon" +_COOKIE_MAX_AGE = 60 * 60 * 24 * 30 # 30 days + + +# Members of these orgs get unlimited usage (like PRO) out of the box. The +# UNLIMITED_ORGS env adds to this set; it doesn't replace it. +_DEFAULT_UNLIMITED_ORGS = {"cerebras", "huggingfacem4", "smolagents", "pollen-robotics"} + + +def _unlimited_orgs() -> "set[str]": + """Org usernames whose members get unlimited usage (like PRO). + + Defaults to {cerebras, HuggingFaceM4, smolagents}; the UNLIMITED_ORGS env + (comma/space-separated, e.g. `UNLIMITED_ORGS=my-team`) adds more. Matched + case-insensitively against the signed-in user's organisations.""" + raw = os.environ.get("UNLIMITED_ORGS", "") + extra = {o.strip().lower() for o in raw.replace(",", " ").split() if o.strip()} + return _DEFAULT_UNLIMITED_ORGS | extra + +try: + from huggingface_hub import attach_huggingface_oauth, parse_huggingface_oauth + _OAUTH_IMPORTABLE = True +except Exception as exc: # pragma: no cover - import guard + logger.info("huggingface_hub OAuth unavailable (%s); sign-in disabled.", exc) + _OAUTH_IMPORTABLE = False + +# Set by attach(): True once OAuth is actually wired (importable + env present). +oauth_enabled = False + + +def attach(app) -> bool: + """Wire HF OAuth onto the app if it's importable and configured. Returns + whether sign-in is available.""" + global oauth_enabled + if not _OAUTH_IMPORTABLE or not os.environ.get("OAUTH_CLIENT_ID"): + return False + try: + attach_huggingface_oauth(app) + oauth_enabled = True + logger.info("HF OAuth attached (sign-in enabled).") + except Exception as exc: # pragma: no cover - defensive + logger.warning("Failed to attach HF OAuth: %r", exc) + oauth_enabled = False + return oauth_enabled + + +def _field(obj, name, default=None): + """Read a field whether the user-info is an object or a dict.""" + if obj is None: + return default + if isinstance(obj, dict): + return obj.get(name, default) + return getattr(obj, name, default) + + +# Surface what we detect (orgs, tier) in logs and on /api/me when set. Handy for +# verifying org gating on the live Space without guessing. +AUTH_DEBUG = bool(os.environ.get("AUTH_DEBUG")) + +# whoami-v2 org lookups are cached for the process lifetime, keyed by token, so +# /api/me + /api/session don't each hit the Hub. +_orgs_cache: "dict[str, set[str]]" = {} + + +def current_oauth(request): + """The parsed HF OAuth info (user_info + access_token), or None.""" + if not oauth_enabled: + return None + try: + return parse_huggingface_oauth(request) + except Exception: + return None + + +def current_user(request): + """The signed-in HF user-info, or None.""" + return _field(current_oauth(request), "user_info") + + +def _user_org_names(user) -> "set[str]": + """The user's organisations from the OAuth userinfo, by username/name/id.""" + names = set() + for org in _field(user, "orgs", []) or []: + for key in ("preferred_username", "name", "sub"): + val = _field(org, key) + if val: + names.add(str(val).lower()) + return names + + +def _orgs_via_token(token: str) -> "set[str]": + """Fallback org lookup via the Hub `whoami-v2` API, using the user's OAuth + access token. Covers the case where the userinfo claim omits `orgs`.""" + if not token: + return set() + if token in _orgs_cache: + return _orgs_cache[token] + names: "set[str]" = set() + try: + import httpx + + resp = httpx.get( + "https://huggingface.co/api/whoami-v2", + headers={"Authorization": f"Bearer {token}"}, + timeout=5.0, + ) + resp.raise_for_status() + for org in resp.json().get("orgs", []) or []: + for key in ("name", "fullname"): + val = org.get(key) + if val: + names.add(str(val).lower()) + except Exception as exc: # pragma: no cover - network/permission dependent + logger.info("whoami-v2 org lookup failed: %r", exc) + _orgs_cache[token] = names + return names + + +def _org_names(user, token=None, allow=None) -> "set[str]": + """The user's org usernames from the OAuth userinfo claim. If that doesn't + already satisfy `allow`, fall back to the Hub `whoami-v2` API (the claim is + often empty or partial), so membership is resolved either way.""" + names = _user_org_names(user) + if token and (allow is None or not (allow & names)): + names = names | _orgs_via_token(token) + return names + + +def resolve_tier(user, token=None) -> str: + """Tier for a signed-in user: 'pro' (paying), 'org' (allow-listed org + member, unlimited), or 'free'. PRO wins over org if both apply.""" + if bool(_field(user, "is_pro", False)): + return "pro" + allow = _unlimited_orgs() + names = _org_names(user, token, allow) + tier = "org" if (allow & names) else "free" + if AUTH_DEBUG: + logger.info("tier=%s orgs=%s allow=%s", tier, sorted(names), sorted(allow)) + return tier + + +def user_view(request) -> dict: + """Public profile for /api/me.""" + info = current_oauth(request) + user = _field(info, "user_info") + if not user: + return {"loggedIn": False, "tier": "anon"} + token = _field(info, "access_token") + out = { + "loggedIn": True, + "username": _field(user, "preferred_username") or _field(user, "name") or "you", + "avatar": _field(user, "picture"), + "tier": resolve_tier(user, token), + } + if AUTH_DEBUG: + out["orgs"] = sorted(_org_names(user, token)) + return out + + +def _client_ip(request) -> str: + """Real client IP. On HF the app sits behind a proxy, so the user's address + is the first hop in X-Forwarded-For, not request.client.host.""" + xff = request.headers.get("x-forwarded-for", "") + if xff: + return xff.split(",")[0].strip() + return request.client.host if request.client else "unknown" + + +def resolve_identity(request): + """Resolve (tier, keys, set_cookie) for this request. + + `keys` are the limiter usage_daily keys to debit (one for signed-in, two for + anonymous). `set_cookie` is a signed value to Set-Cookie when we minted a new + anonymous id, else None. + """ + info = current_oauth(request) + user = _field(info, "user_info") + if user: + sub = _field(user, "sub") or _field(user, "preferred_username") + token = _field(info, "access_token") + return resolve_tier(user, token), [limiter.hash_key(f"sub:{sub}")], None + + # Anonymous: key by IP and a signed cookie id, minting the cookie if absent. + ip = _client_ip(request) + cookie_id = limiter.verify_cookie(request.cookies.get(ANON_COOKIE, "")) + set_cookie = None + if not cookie_id: + cookie_id = secrets.token_urlsafe(18) + set_cookie = limiter.sign_cookie(cookie_id) + keys = [limiter.hash_key(f"ip:{ip}"), limiter.hash_key(f"cookie:{cookie_id}")] + return "anon", keys, set_cookie + + +def set_anon_cookie(response, signed: str) -> None: + # The Space runs inside an iframe on huggingface.co, so the cookie lives in a + # cross-site context — it must be SameSite=None; Secure or the browser drops it. + response.set_cookie( + ANON_COOKIE, signed, + max_age=_COOKIE_MAX_AGE, httponly=True, samesite="none", secure=True, + ) diff --git a/backend/vendor/s2s-cloud/demo/docs/adr/0001-docker-space-with-search-proxy.md b/backend/vendor/s2s-cloud/demo/docs/adr/0001-docker-space-with-search-proxy.md new file mode 100644 index 0000000..740a506 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/docs/adr/0001-docker-space-with-search-proxy.md @@ -0,0 +1,29 @@ +# Convert the static Space into a Docker app with a search proxy + +To give the model a web search tool, the executor (which runs in the browser of a +public Space) needs a search key. A `sdk: static` Space serves files as-is with no +runtime process, so it cannot hold a secret the browser uses without exposing it. +We convert the Space from `sdk: static` to `sdk: docker`: a single container runs a +small server (FastAPI + uvicorn) that both serves the existing front-end *unchanged* +and exposes a same-origin `/search` proxy holding `SERPER_API_KEY` server-side. The +whole app lives in that one container; the s2s speech-to-speech backend stays the +separate load-balanced service it already is. + +## Considered options + +- **Stay static, user-supplied key only** — no owner key; search works only if each + user pastes their own. Rejected as the default because it leaves the deployed demo + with no working search. +- **Separate proxy service** — same secrecy, but splits the app across two deploys. + Rejected: the app must live in one container. +- **Key baked into client JS at build time** — would be readable in the served + bundle. Rejected: defeats the point. + +## Consequences + +- Deployment is no longer static: there is a Dockerfile and a server process; the + README front-matter changes from `sdk: static` to `sdk: docker`. +- The client calls `/search` same-origin; the server reads the key from env. A user + may still supply their own key as a fallback, sent per-request to the proxy. +- The front-end, audio pipeline, and s2s handshake are untouched — only the hosting + shape and the new route are added. diff --git a/backend/vendor/s2s-cloud/demo/index.html b/backend/vendor/s2s-cloud/demo/index.html new file mode 100644 index 0000000..3998b16 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/index.html @@ -0,0 +1,353 @@ + + + + + + Minimal Conversation · S2S backend (WebSocket) + + + + + + + + + + +
+
+
+ + + + + + +
+
+ +
+
+
+ + +
+ + + + +
+ +

Tap to start

+ + + + +
+ + +
+
+ + +
+ + +
+ + + + + +
+
+
+
+

Conversation

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/backend/vendor/s2s-cloud/demo/index.html.orig b/backend/vendor/s2s-cloud/demo/index.html.orig new file mode 100644 index 0000000..dd52bea --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/index.html.orig @@ -0,0 +1,373 @@ + + + + + + Minimal Conversation · S2S backend (WebSocket) + + + + + + + +
+
+
+
+ +

An open, real-time voice chat built on Hugging Face's speech-to-speech stack.

+
+ + Powered by + Inference Endpoints + + Cerebras + + + Built by + Hugging Face + + tfrere + + A-Mahla + + andito + +
+
+
+
+ + + + + + +
+
+ +
+
+
+ + +
+ + + + +
+ +

Tap to start

+ + + +
+ + +
+ + +
+ + + + + +
+
+
+
+

Conversation

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + diff --git a/backend/vendor/s2s-cloud/demo/limiter.py b/backend/vendor/s2s-cloud/demo/limiter.py new file mode 100644 index 0000000..77a7fb2 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/limiter.py @@ -0,0 +1,276 @@ +""" +Per-day talk-time budget for the speech-to-speech demo. + +Our server isn't in the audio path (the browser dials the compute WebSocket +directly), so it can't cut a live stream. What it *can* do is meter time with a +server-clock, chunked reservation: + + - At grant we reserve the first chunk (CHUNK_SEC) and debit it from the day's + budget. A parallel grant therefore sees the budget already spent. + - The client heartbeats; each heartbeat extends the reservation one chunk at a + time until the daily budget runs out, then we report `expired` so the client + tears down. + - On a clean end (sendBeacon) we reconcile to the real elapsed time and refund + the unused chunk. A crash (no end, no heartbeats) is reaped by a sweep and + forfeits at most one chunk. + +All time is the server's clock. Budgets are per UTC day; a new day is simply a +new row (no explicit reset). Logged-in users are keyed by a hashed HF `sub`; +anonymous users by BOTH a hashed IP and a hashed signed-cookie id, OR-matched +(spent = max of the two) so clearing one identifier doesn't reset the budget. + +Storage is SQLite at $USAGE_DB_PATH, else /data (persistent Spaces storage), +else a /tmp fallback. On /tmp the budget is only per-uptime — flagged in logs. +""" + +import hashlib +import hmac +import logging +import math +import os +import sqlite3 +import tempfile +import threading +import time +from datetime import datetime, timezone +from pathlib import Path + +logger = logging.getLogger("s2s.limiter") + +# ── Tunables (env-overridable) ─────────────────────────────────────────────── +ANON_SEC = int(os.environ.get("LIMIT_ANON_SEC", "300")) # 5 min/day, not signed in +FREE_SEC = int(os.environ.get("LIMIT_FREE_SEC", "600")) # 10 min/day, signed in, no PRO +CHUNK_SEC = int(os.environ.get("RESERVE_CHUNK_SEC", "10")) # reservation granularity +HEARTBEAT_SEC = int(os.environ.get("HEARTBEAT_SEC", "5")) # advertised client cadence +REAP_AFTER_SEC = int(os.environ.get("SESSION_REAP_SEC", "15")) # silence before sweep + +# Stable across restarts or the hashed keys (and signed cookies) rotate and the +# budget effectively resets. Set it as a Space secret. Falls back to a per-boot +# random value (keys then only hold within one uptime). +_HASH_SECRET = (os.environ.get("USAGE_HASH_SECRET", "").strip() or os.urandom(32).hex()).encode() + +_lock = threading.Lock() +_db_path: "Path | None" = None + + +def budget_for(tier: str) -> "int | None": + """Daily second-budget for a tier, or None for unlimited. + + Unlimited tiers: 'pro' (paying PRO members) and 'org' (members of an + allow-listed organisation, see UNLIMITED_ORGS in auth.py).""" + if tier in ("pro", "org"): + return None + if tier == "free": + return FREE_SEC + return ANON_SEC + + +def hash_key(raw: str) -> str: + """HMAC a raw identifier (sub / ip / cookie id) into an opaque storage key.""" + digest = hmac.new(_HASH_SECRET, raw.encode("utf-8"), hashlib.sha256).hexdigest() + return f"k_{digest}" + + +def sign_cookie(value: str) -> str: + """`.` so a forged anon-cookie id is rejected on read.""" + sig = hmac.new(_HASH_SECRET, value.encode("utf-8"), hashlib.sha256).hexdigest()[:32] + return f"{value}.{sig}" + + +def verify_cookie(signed: str) -> "str | None": + """Return the id if the signature checks out, else None.""" + if not signed or "." not in signed: + return None + value, _, sig = signed.rpartition(".") + want = hmac.new(_HASH_SECRET, value.encode("utf-8"), hashlib.sha256).hexdigest()[:32] + return value if hmac.compare_digest(sig, want) else None + + +def _today() -> str: + return datetime.now(timezone.utc).date().isoformat() + + +def _resolve_db_path() -> Path: + explicit = os.environ.get("USAGE_DB_PATH", "").strip() + if explicit: + return Path(explicit) + data = Path("/data") + if data.is_dir() and os.access(data, os.W_OK): + return data / "s2s-usage.sqlite3" + logger.warning("No persistent /data — usage budget falls back to /tmp (per-uptime only).") + return Path(tempfile.gettempdir()) / "s2s-usage.sqlite3" + + +def _connect() -> sqlite3.Connection: + con = sqlite3.connect(_db_path, timeout=5.0) + con.execute("PRAGMA journal_mode=WAL") + con.execute("PRAGMA busy_timeout=5000") + return con + + +def init() -> None: + """Create the schema. Call once at startup.""" + global _db_path + _db_path = _resolve_db_path() + with _lock, _connect() as con: + con.execute( + """CREATE TABLE IF NOT EXISTS usage_daily ( + user_key TEXT NOT NULL, + day TEXT NOT NULL, + spent_sec INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL, + PRIMARY KEY (user_key, day) + )""" + ) + con.execute( + """CREATE TABLE IF NOT EXISTS sessions ( + session_id TEXT PRIMARY KEY, + keys TEXT NOT NULL, -- comma-joined usage_daily keys to debit + day TEXT NOT NULL, + tier TEXT NOT NULL, + grant_ts REAL NOT NULL, + last_seen_ts REAL NOT NULL, + reserved_sec INTEGER NOT NULL, + ended INTEGER NOT NULL DEFAULT 0 + )""" + ) + logger.info("Usage limiter ready at %s (anon=%ss free=%ss chunk=%ss)", _db_path, ANON_SEC, FREE_SEC, CHUNK_SEC) + + +# ── Internal helpers (call under _lock) ─────────────────────────────────────── + +def _spent(con, key: str, day: str) -> int: + row = con.execute( + "SELECT spent_sec FROM usage_daily WHERE user_key=? AND day=?", (key, day) + ).fetchone() + return int(row[0]) if row else 0 + + +def _spent_max(con, keys, day: str) -> int: + """OR-match: the most-spent identifier governs.""" + return max((_spent(con, k, day) for k in keys), default=0) + + +def _add(con, keys, day: str, delta: int) -> None: + now = int(time.time()) + for k in keys: + cur = _spent(con, k, day) + nxt = max(0, cur + delta) + con.execute( + """INSERT INTO usage_daily (user_key, day, spent_sec, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(user_key, day) DO UPDATE SET + spent_sec = excluded.spent_sec, updated_at = excluded.updated_at""", + (k, day, nxt, now), + ) + + +# ── Public API ──────────────────────────────────────────────────────────────── + +def remaining(keys, tier: str) -> "int | None": + """Seconds left today for these keys (None = unlimited). No mutation.""" + budget = budget_for(tier) + if budget is None: + return None + with _lock, _connect() as con: + return max(0, budget - _spent_max(con, keys, _today())) + + +def begin(session_id: str, keys, tier: str) -> int: + """Reserve the first chunk for a new session and record it. Returns the + chunk reserved (0 if the budget is already exhausted — the first heartbeat + will then expire it). PRO (unlimited) is never tracked; don't call it here.""" + day = _today() + budget = budget_for(tier) + now = time.time() + with _lock, _connect() as con: + avail = budget - _spent_max(con, keys, day) if budget is not None else CHUNK_SEC + chunk = max(0, min(CHUNK_SEC, avail)) + if chunk: + _add(con, keys, day, chunk) + con.execute( + """INSERT OR REPLACE INTO sessions + (session_id, keys, day, tier, grant_ts, last_seen_ts, reserved_sec, ended) + VALUES (?, ?, ?, ?, ?, ?, ?, 0)""", + (session_id, ",".join(keys), day, tier, now, now, chunk), + ) + return chunk + + +def heartbeat(session_id: str) -> bool: + """Keep a session alive: extend the reservation toward `elapsed + 1 chunk`, + debiting the budget chunk by chunk. Returns True while alive, False once the + budget is spent (caller should tear down) or the session is unknown/ended.""" + now = time.time() + with _lock, _connect() as con: + row = con.execute( + "SELECT keys, day, tier, grant_ts, reserved_sec, ended FROM sessions WHERE session_id=?", + (session_id,), + ).fetchone() + if not row or row[5]: + return False + keys = row[0].split(",") + day, tier, grant_ts, reserved = row[1], row[2], row[3], int(row[4]) + budget = budget_for(tier) + elapsed = now - grant_ts + + # Grow the reservation one chunk at a time until it covers elapsed + a + # one-chunk lookahead, or the budget runs dry. + while reserved < elapsed + CHUNK_SEC: + if budget is not None and _spent_max(con, keys, day) >= budget: + break + _add(con, keys, day, CHUNK_SEC) + reserved += CHUNK_SEC + + alive = reserved > elapsed # could we cover the time already elapsed? + con.execute( + "UPDATE sessions SET last_seen_ts=?, reserved_sec=?, ended=? WHERE session_id=?", + (now, reserved, 0 if alive else 1, session_id), + ) + if not alive: + _reconcile(con, keys, day, grant_ts, reserved, end_ts=now) + return alive + + +def end(session_id: str) -> None: + """Clean teardown: reconcile to actual elapsed time and refund the unused + reservation. Idempotent.""" + now = time.time() + with _lock, _connect() as con: + row = con.execute( + "SELECT keys, day, grant_ts, reserved_sec, ended FROM sessions WHERE session_id=?", + (session_id,), + ).fetchone() + if not row or row[4]: + return + keys, day, grant_ts, reserved = row[0].split(","), row[1], row[2], int(row[3]) + _reconcile(con, keys, day, grant_ts, reserved, end_ts=now) + con.execute("UPDATE sessions SET ended=1, last_seen_ts=? WHERE session_id=?", (now, session_id)) + + +def sweep() -> None: + """Reap sessions that went silent (crash / closed without a beacon): bill + their elapsed time, refund the rest, mark ended. Forfeits ≤ one chunk.""" + now = time.time() + cutoff = now - REAP_AFTER_SEC + with _lock, _connect() as con: + stale = con.execute( + "SELECT session_id, keys, day, grant_ts, last_seen_ts, reserved_sec FROM sessions " + "WHERE ended=0 AND last_seen_ts < ?", + (cutoff,), + ).fetchall() + for session_id, keys_s, day, grant_ts, last_seen, reserved in stale: + _reconcile(con, keys_s.split(","), day, grant_ts, int(reserved), end_ts=last_seen) + con.execute("UPDATE sessions SET ended=1 WHERE session_id=?", (session_id,)) + if stale: + logger.debug("swept %d stale session(s)", len(stale)) + + +def _reconcile(con, keys, day: str, grant_ts: float, reserved: int, end_ts: float) -> None: + """Refund reserved-but-unused time. Bill elapsed rounded up to a chunk, + capped at what was reserved.""" + elapsed = max(0.0, end_ts - grant_ts) + billed = min(reserved, int(math.ceil(elapsed / CHUNK_SEC) * CHUNK_SEC)) + refund = reserved - billed + if refund > 0: + _add(con, keys, day, -refund) diff --git a/backend/vendor/s2s-cloud/demo/main.js b/backend/vendor/s2s-cloud/demo/main.js new file mode 100644 index 0000000..dd5e3b1 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/main.js @@ -0,0 +1,1524 @@ +// @ts-check +/** + * Minimal voice conversation app, talking to a Hugging Face speech-to-speech + * backend over **WebSocket** (drop-in alternative to the WebRTC variant). + * + * Click the orb -> we ask for the mic, POST a session on the LB, open a + * WebSocket on the routed compute endpoint, push session.update + mic + * audio, play back the TTS audio. The orb visually reflects the live + * state (idle, connecting, listening, user-speaking, processing, + * ai-speaking). + * + * The only meaningful difference vs. the WebRTC main.js is that the + * client owns its own AudioContext (no `attachOutputTrack`), so we hand + * it the MediaStream directly. + * + * @typedef {"idle" | "connecting" | "queued" | "your-turn" | "listening" | "user-speaking" | "processing" | "ai-speaking" | "error"} AppState + */ + +import { S2sWsRealtimeClient } from "./ws/s2s-ws-client.js"; +import { $, truncateError, DEBUG } from "./ui/dom.js"; +import { ChatView } from "./ui/chat.js"; +import { Account } from "./ui/account.js"; + +const DEFAULT_VOICE = "Aiden"; +const DEFAULT_INSTRUCTIONS = + "你是一个友好的中文语音助手。请用中文简洁地回答用户的问题,语气温暖自然。"; + +// Appended to the user's instructions whenever at least one tool is enabled. +// Stops the model from announcing capabilities ("Yes, I can search") and then +// idling for the next turn — it should act immediately in the same response. +const TOOL_USE_HINT = + " When the user's request calls for one of your tools, do not describe your " + + "capabilities or say you can do it and wait for another turn. Instead, say " + + 'a brief acknowledgement like "Let me search for that..." and call the tool ' + + "right away in the same response."; + +const STORAGE_KEYS = { + // Direct s2s server URL, used only when the deploy has no LOAD_BALANCER_URL + // (in LB mode the browser never learns the LB address — it POSTs /api/session). + directUrl: "s2s.ws.directUrl", + voice: "s2s.ws.voice", + instructions: "s2s.ws.instructions", + tools: "s2s.ws.tools", + searchKey: "s2s.ws.searchKey", + noiseGate: "s2s.ws.noiseGate", +}; + +// ── Noise gate ────────────────────────────────────────────────────────────── +// The Settings cursor sets the gate's open threshold in dBFS. Its leftmost +// position is an OFF detent (gate disabled, pure passthrough); the rest of the +// travel is the active threshold. The cursor shares the meter's dB axis, so the +// handle sits on the level bar — raise it until room noise stops lighting it up. +// The slider range IS the shared axis: the live meter fill and the threshold +// thumb both map across [GATE_OFF_DB, GATE_MAX_DB], so the thumb sits exactly +// where the gate cuts on the same scale as the level bar. +const GATE_OFF_DB = -66; // slider minimum = off / bottom of the meter axis +const GATE_MAX_DB = -3; // slider maximum = most aggressive / top of the meter axis +const GATE_DEFAULT_DB = -50; // first-run default: a gentle gate, enabled + +/** @param {number} thresholdDb @returns {import("./ws/s2s-ws-client.js").NoiseGate} */ +function gateParams(thresholdDb) { + return { enabled: thresholdDb > GATE_OFF_DB, thresholdDb }; +} + +// ── Tools ───────────────────────────────────────────────────────────────── +// Function tools we declare to the backend. The model decides when to call +// one; the executor below runs it and returns the result (see runTool). +/** @type {Record} */ +const TOOL_DEFS = { + web_search: { + type: "function", + name: "web_search", + description: + "Search the web for current or factual information you don't already know " + + "(news, prices, facts, documentation). Returns the top results with titles, " + + "snippets and URLs.", + parameters: { + type: "object", + properties: { query: { type: "string", description: "The search query." } }, + required: ["query"], + }, + }, + camera_snapshot: { + type: "function", + name: "camera_snapshot", + description: + "Capture the current frame from the user's webcam so you can see what they " + + "are showing you. Use it whenever the user refers to something visual or " + + "asks you to look.", + parameters: { type: "object", properties: {}, required: [] }, + }, +}; + +/** Longest edge of the snapshot sent to the VLM, in px (keeps payload sane). */ +const SNAPSHOT_MAX_EDGE = 768; +const SNAPSHOT_QUALITY = 0.7; + +function loadSettings() { + return { + directUrl: localStorage.getItem(STORAGE_KEYS.directUrl) || "", + voice: localStorage.getItem(STORAGE_KEYS.voice) || DEFAULT_VOICE, + instructions: localStorage.getItem(STORAGE_KEYS.instructions) || DEFAULT_INSTRUCTIONS, + noiseGate: loadGateThreshold(), + }; +} + +/** Stored gate threshold (dBFS), clamped to the slider range. Defaults to a + * gentle enabled gate (GATE_DEFAULT_DB) when the user hasn't set one yet. */ +function loadGateThreshold() { + const stored = localStorage.getItem(STORAGE_KEYS.noiseGate); + // getItem returns null when unset, and Number(null) === 0 (finite!), so guard + // the missing/empty case explicitly before coercing — otherwise the default + // never fires and 0 clamps to the slider max. + if (stored === null || stored === "") return GATE_DEFAULT_DB; + const raw = Number(stored); + if (!Number.isFinite(raw)) return GATE_DEFAULT_DB; + return Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, Math.round(raw))); +} + +/** @param {ReturnType} s */ +function saveSettings(s) { + localStorage.setItem(STORAGE_KEYS.directUrl, s.directUrl); + localStorage.setItem(STORAGE_KEYS.voice, s.voice); + localStorage.setItem(STORAGE_KEYS.instructions, s.instructions); + localStorage.setItem(STORAGE_KEYS.noiseGate, String(s.noiseGate)); +} + +/** @returns {{ web_search: boolean, camera_snapshot: boolean }} */ +function loadTools() { + try { + const raw = JSON.parse(localStorage.getItem(STORAGE_KEYS.tools) || "{}"); + // Both tools default ON (web search still only activates when a key exists). + // We never call getUserMedia on page load — the camera only actually starts + // on a user gesture (conversation start), so a default-on flag doesn't + // silently resume the webcam; an explicit saved `false` is respected. + return { + web_search: raw.web_search ?? true, + camera_snapshot: raw.camera_snapshot ?? true, + }; + } catch { + return { web_search: true, camera_snapshot: true }; + } +} + +function saveTools() { + localStorage.setItem(STORAGE_KEYS.tools, JSON.stringify(toolsEnabled)); +} + +/** @type {Record} */ +const STATE_VIEWS = { + idle: { caption: "Tap to start", disabled: false }, + connecting: { caption: "Connecting", disabled: true }, + queued: { caption: "Finding you a spot…", disabled: true }, + "your-turn": { caption: "You're up! 🎉", disabled: true }, + listening: { caption: "", disabled: false }, + "user-speaking": { caption: "", disabled: false }, + processing: { caption: "", disabled: false }, + "ai-speaking": { caption: "", disabled: false }, + error: { caption: "Tap to retry", disabled: false }, +}; + +/** @type {Record} */ +const STATE_CLASS = { + idle: "state-idle", + connecting: "state-connecting", + queued: "state-queued", + "your-turn": "state-your-turn", + listening: "state-listening", + "user-speaking": "state-user-speaking", + processing: "state-processing", + "ai-speaking": "state-ai-speaking", + error: "state-error", +}; + +/** @type {ReadonlySet} */ +const LIVE_STATES = new Set(["listening", "user-speaking", "processing", "ai-speaking"]); + +/** @type {HTMLButtonElement} */ +const circleBtn = $("#main-circle"); +/** @type {HTMLParagraphElement} */ +const circleCaption = $("#circle-caption"); +/** @type {HTMLParagraphElement} */ +const circleSubcaption = $("#circle-subcaption"); +/** @type {HTMLElement} */ +const orbWrap = $(".orb-wrap"); +/** @type {HTMLButtonElement} */ +const micBtn = $("#mic-btn"); +/** @type {HTMLButtonElement} */ +const stopBtn = $("#stop-btn"); +/** @type {HTMLElement} */ +const queueActions = $("#queue-actions"); +/** @type {HTMLButtonElement} */ +const joinQueueBtn = $("#join-queue-btn"); +/** @type {HTMLButtonElement} */ +const leaveQueueBtn = $("#leave-queue-btn"); + +/** @type {HTMLButtonElement} */ +const settingsBtn = $("#settings-btn"); +/** @type {HTMLDialogElement} */ +const settingsModal = $("#settings-modal"); + +/** @type {HTMLDialogElement} */ +const aboutModal = $("#about-modal"); +/** @type {HTMLButtonElement} */ +const aboutClose = $("#about-close"); + +/** @type {HTMLButtonElement} */ +const toolsBtn = $("#tools-btn"); +/** @type {HTMLDialogElement} */ +const toolsModal = $("#tools-modal"); +/** @type {HTMLButtonElement} */ +const toolsClose = $("#tools-close"); +/** @type {HTMLInputElement} */ +const toolWebSwitch = $("#tool-web"); +/** @type {HTMLInputElement} */ +const toolCamSwitch = $("#tool-cam"); +/** @type {HTMLElement} */ +const toolWebRow = $("#tool-web-row"); +/** @type {HTMLElement} */ +const toolWebHint = $("#tool-web-hint"); +/** @type {HTMLElement} */ +const toolCamHint = $("#tool-cam-hint"); +/** @type {HTMLInputElement} */ +const searchKeyInput = $("#search-key"); +/** @type {HTMLElement} */ +const camPip = $("#cam-pip"); +/** @type {HTMLVideoElement} */ +const camVideo = $("#cam-video"); + +/** @type {HTMLInputElement} */ +const inputLbUrl = $("#lb-url"); +/** @type {HTMLElement} */ +const connField = $("#conn-field"); +/** @type {HTMLElement} */ +const connHint = $("#conn-hint"); +/** @type {HTMLSelectElement} */ +const inputVoice = $("#voice"); +/** @type {HTMLInputElement} */ +const inputVoiceUpload = $("#voice-upload"); +/** @type {HTMLElement} */ +const voiceUploadBtn = $("#voice-upload-btn"); +/** @type {HTMLTextAreaElement} */ +const inputInstructions = $("#instructions"); + +// Voice upload: clicking the Upload button opens the file picker +voiceUploadBtn.addEventListener("click", () => inputVoiceUpload.click()); +voiceUploadBtn.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === " ") inputVoiceUpload.click(); +}); +// After selecting a file, upload it and set as current voice +inputVoiceUpload.addEventListener("change", async () => { + const file = inputVoiceUpload.files?.[0]; + if (!file) return; + try { + voiceUploadBtn.textContent = "⏳ Uploading..."; + const form = new FormData(); + form.append("file", file); + const resp = await fetch("/api/upload_voice", { method: "POST", body: form }); + if (!resp.ok) throw new Error(`Upload failed: ${resp.status}`); + const data = await resp.json(); + // Add uploaded voice as a selectable option + const opt = document.createElement("option"); + opt.value = data.path; + opt.textContent = `📁 ${data.filename}`; + inputVoice.insertBefore(opt, inputVoice.firstChild); + inputVoice.value = data.path; + voiceUploadBtn.textContent = "✓ Uploaded"; + setCaption(`Voice sample "${data.filename}" uploaded — restart to apply`, "muted"); + } catch (err) { + setCaption(`Upload error: ${err.message}`, "error"); + voiceUploadBtn.textContent = "📤 Upload"; + } + inputVoiceUpload.value = ""; + setTimeout(() => { voiceUploadBtn.textContent = "📤 Upload"; }, 3000); +}); +/** @type {HTMLInputElement} */ +const inputNoiseGate = $("#noise-gate"); +/** @type {HTMLElement} */ +const gateValue = $("#gate-value"); +/** @type {HTMLElement} */ +const gateMeterFill = $("#gate-meter-fill"); +/** @type {HTMLElement} */ +const micGate = $("#mic-gate"); +const mgaArc = /** @type {SVGSVGElement} */ (document.querySelector("#mic-gate-arc")); +const mgaTrack = /** @type {SVGPathElement} */ (document.querySelector("#mga-track")); +const mgaFill = /** @type {SVGPathElement} */ (document.querySelector("#mga-fill")); +const mgaHit = /** @type {SVGPathElement} */ (document.querySelector("#mga-hit")); +const mgaHandle = /** @type {SVGCircleElement} */ (document.querySelector("#mga-handle")); + +/** @type {HTMLButtonElement} */ +const restartBtn = $("#restart-conversation"); +/** @type {HTMLElement} */ +const restartHint = $("#restart-hint"); +const settingsForm = /** @type {HTMLFormElement} */ (settingsModal.querySelector("form")); + +/** @type {AppState} */ +let currentState = "idle"; +let settings = loadSettings(); + +// ── Connection target ──────────────────────────────────────────────────────── +// Three modes, decided by the deploy via /api/config: +// • SPEECH_TO_SPEECH_URL set -> direct mode pinned by the deploy: the browser +// connects straight to that URL, shown read-only in Settings. Overrides the +// load balancer entirely. +// • LOAD_BALANCER_URL set -> original flow: POST the same-origin /api/session +// proxy (the server forwards to the LB; the LB address is never sent here). +// • neither (allowDirect) -> the user sets a speech-to-speech server URL and +// the browser connects to it directly (no load balancer, no /session). +let lbMode = false; +// Fail open: direct entry is allowed unless /api/config reports an LB URL. This +// way a missing/unreachable config (e.g. static hosting) leaves the field +// usable rather than locked. +let allowDirect = true; +// Deploy-pinned s2s URL (SPEECH_TO_SPEECH_URL). Non-empty -> locked direct +// mode: the field displays it read-only and the saved user URL is untouched. +let pinnedUrl = ""; + +// ── Tool state ────────────────────────────────────────────────────────────── +let toolsEnabled = loadTools(); +// Whether the server holds a Serper key (learned from /api/config on load). +let serverSearchKey = false; +// A user-supplied key (fallback when the deploy has none). localStorage only. +let userSearchKey = localStorage.getItem(STORAGE_KEYS.searchKey) || ""; +/** @type {MediaStream | null} */ +let cameraStream = null; + +/** Search is usable if the server has a key or the user supplied one. */ +function searchAvailable() { + return serverSearchKey || !!userSearchKey; +} + +/** Tool definitions for the currently-enabled (and usable) tools. */ +function activeToolDefs() { + const defs = []; + if (toolsEnabled.web_search && searchAvailable()) defs.push(TOOL_DEFS.web_search); + if (toolsEnabled.camera_snapshot) defs.push(TOOL_DEFS.camera_snapshot); + return defs; +} + +/** Instructions plus the hidden tool-use hint when any tool is active. */ +function effectiveInstructions() { + const base = settings.instructions; + return activeToolDefs().length ? base + TOOL_USE_HINT : base; +} + +/** Push the active tool set to a live session so toggles apply mid-call. */ +function pushToolsToSession() { + if (!client || !LIVE_STATES.has(currentState)) return; + client.setTools(activeToolDefs()); + // The hidden tool-use hint depends on whether any tool is active, so refresh + // instructions alongside the tool set. + client.updateSession({ instructions: effectiveInstructions() }); +} + +// ── Chat view ─────────────────────────────────────────────────────────────── +// Owns the history panel, the ephemeral bubbles, and all transcript/tool +// streaming state. The client's events are forwarded to its on* methods. +const chat = new ChatView(); + +// ── Account / limiter ───────────────────────────────────────────────────── +// Login chip + daily-limit modal (inert unless the deploy is in LB mode). The +// server meters conversation time; the client just heartbeats a live session +// and tears down when the server reports the budget is spent. +const account = new Account(); +let limiterOn = false; +let heartbeatTimer = 0; +let trackedSessionId = ""; +let trackedTier = ""; +// The waiting-queue ticket id while we're in line (else ""). Used to leave the +// queue on teardown / tab-close so we don't hold a phantom place. +let queuedTicketId = ""; + +/** @type {S2sWsRealtimeClient | null} */ +let client = null; +/** @type {MediaStream | null} */ +let micStream = null; +let micMuted = false; + +/** @param {AppState} next */ +function setState(next) { + currentState = next; + const view = STATE_VIEWS[next]; + circleBtn.disabled = view.disabled; + circleBtn.className = `circle ${STATE_CLASS[next]}`; + if (next !== "error") setCaption(view.caption); + + const live = LIVE_STATES.has(next); + orbWrap.classList.toggle("live", live); + micBtn.setAttribute("aria-hidden", live ? "false" : "true"); + stopBtn.setAttribute("aria-hidden", live ? "false" : "true"); + micBtn.tabIndex = live ? 0 : -1; + stopBtn.tabIndex = live ? 0 : -1; + + // Queue affordances: "Leave queue" whenever we're in line; "Join now" only once + // it's our turn (a slot is held for us). Both live under #queue-actions. + const yourTurn = next === "your-turn"; + const inLine = next === "queued" || yourTurn; + queueActions.hidden = !inLine; + joinQueueBtn.hidden = !yourTurn; + joinQueueBtn.tabIndex = yourTurn ? 0 : -1; + leaveQueueBtn.hidden = !inLine; + leaveQueueBtn.tabIndex = inLine ? 0 : -1; + if (!yourTurn) stopJoinCountdown(); + + // Warm reassurance under the terse position, only while waiting in line. + if (next === "queued") { + circleSubcaption.textContent = + "Sorry, we overhugged! 🤗 Every slot is busy, so we saved you a spot. Hang tight, you're moving up."; + circleSubcaption.hidden = false; + } else { + circleSubcaption.hidden = true; + } + + updateRestartAvailability(); +} + +function updateRestartAvailability() { + // Restart works from any settled state — it tears down a live call (if any) + // and reconnects with the current settings. Only block while mid-connect or + // while waiting in the queue (restarting from there would just re-queue). + restartBtn.disabled = + currentState === "connecting" || currentState === "queued" || currentState === "your-turn"; + restartHint.hidden = false; + restartHint.textContent = LIVE_STATES.has(currentState) + ? "Reconnects now with the settings above." + : "Starts a conversation with the settings above."; +} + +/** + * @param {string} text + * @param {"" | "error" | "muted"} [kind] + */ +function setCaption(text, kind = "") { + const trimmed = text.trim(); + circleCaption.textContent = trimmed; + circleCaption.className = `circle-caption${kind ? ` ${kind}` : ""}${trimmed ? "" : " empty"}`; +} + +function openSettings() { + syncConnectionUi(); + inputVoice.value = settings.voice; + inputInstructions.value = settings.instructions; + syncGateUi(); + updateRestartAvailability(); + settingsModal.showModal(); +} + +/** dB position (clamped to the slider axis) as a 0..1 fraction of the track. + * @param {number} db */ +function dbToFraction(db) { + const clamped = Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, db)); + return (clamped - GATE_OFF_DB) / (GATE_MAX_DB - GATE_OFF_DB); +} + +/** @param {number} f @returns {number} dB at a 0..1 position on the gate axis. */ +function fractionToDb(f) { + const clamped = Math.min(1, Math.max(0, f)); + return Math.round(GATE_OFF_DB + clamped * (GATE_MAX_DB - GATE_OFF_DB)); +} + +// ── Radial gate arc (around the mic button, live during a call) ───────────── +// A 270° arc with the gap facing the orb (right). Fraction 0 (=Off) sits at the +// bottom-ish start; 1 (=max) at the top-ish end. The level fill and the +// threshold handle ride this same axis, mirroring the Settings widget. +const ARC_R = 40; +// A ~200° arc centred on the left (180°) so the wide gap faces the orb (right). +const ARC_SPAN_DEG = 200; +const ARC_START_DEG = 180 - ARC_SPAN_DEG / 2; // lower-left start; Off end + +/** Point at fraction f (0..1) and radius r, in the 0..100 viewBox. + * @param {number} f @param {number} [r] */ +function arcPoint(f, r = ARC_R) { + const deg = ARC_START_DEG + f * ARC_SPAN_DEG; + const rad = (deg * Math.PI) / 180; + return { x: 50 + r * Math.cos(rad), y: 50 + r * Math.sin(rad) }; +} + +/** SVG path `d` for the full 0..1 arc (clockwise). */ +function fullArcD() { + const a = arcPoint(0); + const b = arcPoint(1); + const largeArc = ARC_SPAN_DEG > 180 ? 1 : 0; + return `M ${a.x} ${a.y} A ${ARC_R} ${ARC_R} 0 ${largeArc} 1 ${b.x} ${b.y}`; +} + +/** One-time geometry: track, fill (dash-revealed) and the transparent hit band. */ +function initGateArc() { + const d = fullArcD(); + mgaTrack.setAttribute("d", d); + mgaFill.setAttribute("d", d); + mgaHit.setAttribute("d", d); + // pathLength 100 lets us reveal the fill by fraction via dashoffset. + mgaFill.setAttribute("pathLength", "100"); + mgaFill.style.strokeDasharray = "100 100"; + mgaFill.style.strokeDashoffset = "100"; // empty until levels arrive + renderGateHandle(); +} + +/** Place the threshold bead on the arc at the stored threshold; flag off state. */ +function renderGateHandle() { + const off = settings.noiseGate <= GATE_OFF_DB; + const p = arcPoint(dbToFraction(settings.noiseGate)); + mgaHandle.setAttribute("cx", String(p.x)); + mgaHandle.setAttribute("cy", String(p.y)); + micGate.classList.toggle("gate-off", off); +} + +/** Paint a 0..1 live level onto the arc fill (and the Settings meter if open). + * Brightens the tick when the level crosses the threshold — i.e. the gate is + * actually open — but only when gating is enabled. + * @param {number} rms */ +function paintInputLevel(rms) { + const db = rms > 0 ? 20 * Math.log10(rms) : GATE_OFF_DB; + const f = dbToFraction(db); + mgaFill.style.strokeDashoffset = String(100 * (1 - f)); + if (settingsModal.open) gateMeterFill.style.width = `${f * 100}%`; + const enabled = settings.noiseGate > GATE_OFF_DB; + micGate.classList.toggle("gate-open", enabled && f >= dbToFraction(settings.noiseGate)); +} + +/** The single place that commits a new gate threshold: updates both controls, + * persists, and applies live to the running session. + * @param {number} db */ +function setGateThreshold(db) { + settings.noiseGate = Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, Math.round(db))); + const off = settings.noiseGate <= GATE_OFF_DB; + inputNoiseGate.value = String(settings.noiseGate); + gateValue.textContent = off ? "Off" : `${settings.noiseGate} dB`; + renderGateHandle(); + localStorage.setItem(STORAGE_KEYS.noiseGate, String(settings.noiseGate)); + if (client && LIVE_STATES.has(currentState)) { + client.setNoiseGate(gateParams(settings.noiseGate)); + } +} + +/** Reflect the stored gate threshold into the slider, label and arc handle. */ +function syncGateUi() { + inputNoiseGate.value = String(settings.noiseGate); + const off = settings.noiseGate <= GATE_OFF_DB; + gateValue.textContent = off ? "Off" : `${settings.noiseGate} dB`; + renderGateHandle(); +} + +// Drag along the arc band to set the threshold (a tap on the glyph still mutes). +let gateDragging = false; +/** @param {PointerEvent} e */ +function gatePointerToDb(e) { + const rect = mgaArc.getBoundingClientRect(); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + let deg = (Math.atan2(e.clientY - cy, e.clientX - cx) * 180) / Math.PI; + if (deg < 0) deg += 360; + // Map the on-arc angle to a fraction; angles in the right-side gap fall + // outside [0,1] and fractionToDb clamps them to the nearest end (just-below + // start -> Off, just-past end -> max). + const f = (deg - ARC_START_DEG) / ARC_SPAN_DEG; + return fractionToDb(f); +} +mgaHit.addEventListener("pointerdown", (e) => { + gateDragging = true; + mgaHit.setPointerCapture(e.pointerId); + setGateThreshold(gatePointerToDb(e)); +}); +mgaHit.addEventListener("pointermove", (e) => { + if (gateDragging) setGateThreshold(gatePointerToDb(e)); +}); +const endGateDrag = (/** @type {PointerEvent} */ e) => { + if (!gateDragging) return; + gateDragging = false; + try { mgaHit.releasePointerCapture(e.pointerId); } catch {} +}; +mgaHit.addEventListener("pointerup", endGateDrag); +mgaHit.addEventListener("pointercancel", endGateDrag); + +settingsBtn.addEventListener("click", openSettings); + +// About panel: native , Esc closes for free; also close on the X and +// on a click in the backdrop (a click whose target is the dialog itself). +$("#about-btn-m").addEventListener("click", () => aboutModal.showModal()); +aboutClose.addEventListener("click", () => aboutModal.close()); +aboutModal.addEventListener("click", (e) => { + if (e.target === aboutModal) aboutModal.close(); +}); + +// ── Tools panel ─────────────────────────────────────────────────────────── + +/** Reflect the current tool state into the panel controls. */ +function syncToolsUi() { + const avail = searchAvailable(); + toolWebSwitch.checked = toolsEnabled.web_search && avail; + toolWebSwitch.disabled = !avail; + toolWebRow.classList.toggle("disabled", !avail); + toolCamSwitch.checked = toolsEnabled.camera_snapshot; + + if (serverSearchKey) { + // Key lives server-side: show it as configured, never expose it. + searchKeyInput.value = ""; + searchKeyInput.placeholder = "•••••••• · provided by the server"; + searchKeyInput.disabled = true; + toolWebHint.textContent = "Ready. The search key is held server-side and never sent to your browser."; + } else { + searchKeyInput.disabled = false; + searchKeyInput.value = userSearchKey; + searchKeyInput.placeholder = "Paste a Serper key to enable web search"; + toolWebHint.textContent = userSearchKey + ? "Using your key — stored in this browser only." + : "No server key configured. Add your own Serper key to enable web search."; + } +} + +toolsBtn.addEventListener("click", () => { syncToolsUi(); toolsModal.showModal(); }); +toolsClose.addEventListener("click", () => toolsModal.close()); +toolsModal.addEventListener("click", (e) => { + if (e.target === toolsModal) toolsModal.close(); +}); + +toolWebSwitch.addEventListener("change", () => { + if (toolWebSwitch.checked && !searchAvailable()) { + toolWebSwitch.checked = false; // guard: can't enable without a key + return; + } + toolsEnabled.web_search = toolWebSwitch.checked; + saveTools(); + pushToolsToSession(); +}); + +toolCamSwitch.addEventListener("change", async () => { + if (toolCamSwitch.checked) { + try { + // Flipping the switch always re-requests the camera, so a permission that + // was only dismissed earlier is asked again here. + await enableCamera(); + } catch (err) { + toolCamSwitch.checked = false; + const denied = err instanceof Error && (err.name === "NotAllowedError" || err.name === "SecurityError"); + toolCamHint.textContent = denied + ? "Camera blocked. Allow it from the camera icon in your browser's address bar — it switches on automatically." + : `Camera unavailable${err instanceof Error ? `: ${err.message}` : ""}`; + return; + } + toolsEnabled.camera_snapshot = true; + toolCamHint.textContent = "Camera on. The assistant can take a snapshot when it needs to see."; + } else { + disableCamera(); + toolsEnabled.camera_snapshot = false; + toolCamHint.textContent = "Let the assistant see through your webcam."; + } + saveTools(); + pushToolsToSession(); +}); + +searchKeyInput.addEventListener("input", () => { + if (serverSearchKey) return; + userSearchKey = searchKeyInput.value.trim(); + if (userSearchKey) localStorage.setItem(STORAGE_KEYS.searchKey, userSearchKey); + else localStorage.removeItem(STORAGE_KEYS.searchKey); + + const avail = searchAvailable(); + toolWebSwitch.disabled = !avail; + toolWebRow.classList.toggle("disabled", !avail); + // Losing the key disables a previously-enabled tool. + if (!avail && toolsEnabled.web_search) { + toolsEnabled.web_search = false; + toolWebSwitch.checked = false; + saveTools(); + pushToolsToSession(); + } + toolWebHint.textContent = userSearchKey + ? "Using your key — stored in this browser only." + : "No server key configured. Add your own Serper key to enable web search."; +}); + +// ── Camera ────────────────────────────────────────────────────────────────── + +async function enableCamera() { + if (cameraStream) return; + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: "user" }, + audio: false, + }); + camVideo.srcObject = cameraStream; + try { await camVideo.play(); } catch { /* autoplay quirks; muted video is fine */ } + camPip.classList.add("visible"); + camPip.setAttribute("aria-hidden", "false"); + // Lets the footer reflow to the bottom-right (and hide on mobile) while the + // webcam preview occupies the bottom of the stage. + document.body.classList.add("cam-on"); +} + +function disableCamera() { + if (cameraStream) { + for (const t of cameraStream.getTracks()) t.stop(); + cameraStream = null; + } + camVideo.srcObject = null; + camPip.classList.remove("visible"); + camPip.setAttribute("aria-hidden", "true"); + document.body.classList.remove("cam-on"); +} + +/** Auto-start the webcam on arrival (the camera tool is on by default). If the + * user declines the permission, switch the tool off and reflect it in the UI + * rather than nagging. */ +async function autoStartCamera() { + if (!toolsEnabled.camera_snapshot || cameraStream) return; + try { + await enableCamera(); + } catch (err) { + console.warn("[main] camera auto-start declined/failed:", err); + toolsEnabled.camera_snapshot = false; + saveTools(); + syncToolsUi(); + } +} + +/** Track the browser's camera permission so a later re-grant (e.g. the user + * unblocks it from the address bar after a denial) turns the camera back on + * without another toggle, and a revoke turns it off. Best-effort: the + * Permissions API doesn't support "camera" everywhere (e.g. Safari). */ +async function watchCameraPermission() { + try { + const status = await navigator.permissions?.query?.({ name: /** @type {any} */ ("camera") }); + if (!status) return; + status.addEventListener("change", () => { + if (status.state === "granted") { + if (!toolsEnabled.camera_snapshot) { toolsEnabled.camera_snapshot = true; saveTools(); } + void autoStartCamera(); + syncToolsUi(); + } else if (status.state === "denied") { + disableCamera(); + if (toolsEnabled.camera_snapshot) { toolsEnabled.camera_snapshot = false; saveTools(); } + syncToolsUi(); + } + }); + } catch { + // Permissions API unavailable for "camera" — the toggle still re-asks. + } +} + +/** + * Grab the current webcam frame as a downscaled JPEG data URL. The preview is + * mirrored in CSS for a natural self-view, but we draw the raw (un-mirrored) + * video here so the model sees the scene in its true orientation. + * @returns {string | null} + */ +function captureSnapshot() { + if (!cameraStream || !camVideo.videoWidth) return null; + const vw = camVideo.videoWidth; + const vh = camVideo.videoHeight; + const scale = Math.min(1, SNAPSHOT_MAX_EDGE / Math.max(vw, vh)); + const w = Math.max(1, Math.round(vw * scale)); + const h = Math.max(1, Math.round(vh * scale)); + const canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d"); + if (!ctx) return null; + ctx.drawImage(camVideo, 0, 0, w, h); + return canvas.toDataURL("image/jpeg", SNAPSHOT_QUALITY); +} + +/** Brief shutter flash on the preview so the user sees a snapshot was taken. */ +function flashPreview() { + camPip.classList.remove("flash"); + void camPip.offsetWidth; // reflow so the animation restarts + camPip.classList.add("flash"); +} + +// ── Tool executor ───────────────────────────────────────────────────────── +// Runs the function the model called, returns the result, and asks for a +// response so the model speaks it. Errors come back as the tool output too, so +// the model can recover gracefully instead of the turn stalling. + +/** + * Run the function the model called, return its result to the backend, and ask + * for a follow-up response. We also hand the result back to the caller so it + * can be shown in the conversation once the tool has actually run. + * @param {string} name @param {string} argsJson @param {string} callId + * @returns {Promise<{ output: string, image?: string }>} + */ +async function runTool(name, argsJson, callId) { + if (!client) return { output: "" }; + let args = /** @type {Record} */ ({}); + try { args = JSON.parse(argsJson || "{}"); } catch { /* keep {} */ } + + if (DEBUG) console.debug(`[tool] run name=${name} callId=${JSON.stringify(callId)} args=${argsJson}`); + if (!callId) console.warn("[tool] empty call_id — the backend didn't tag the call, can't return a function_call_output"); + + /** @type {{ output: string, image?: string }} */ + let result = { output: "" }; + try { + if (name === "web_search") { + const query = typeof args.query === "string" ? args.query : ""; + result.output = await execWebSearch(query); + // Return the result and let the bare response.create (below) trigger the + // spoken answer. + client.sendToolOutput(callId, result.output); + } else if (name === "camera_snapshot") { + const dataUrl = captureSnapshot(); + if (dataUrl) { + if (DEBUG) console.debug(`[tool] camera_snapshot captured frame (${dataUrl.length} chars), sending image + output`); + result = { output: "Snapshot captured from the webcam and attached as an image.", image: dataUrl }; + // Return the tool output; the frame itself rides along with the + // response.create below (sent right before it), so the model sees the + // snapshot in the very response it's about to speak. + client.sendToolOutput(callId, result.output); + flashPreview(); + } else { + console.warn("[tool] camera_snapshot: no frame — camera off or not ready"); + result.output = "The camera is not available right now."; + client.sendToolOutput(callId, result.output); + } + } else { + result.output = `Unknown tool: ${name}`; + client.sendToolOutput(callId, result.output); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + result.output = `Tool failed: ${msg}`; + client.sendToolOutput(callId, result.output); + } + if (DEBUG) console.debug(`[tool] requesting model response after ${name}`); + // Camera: the captured frame rides with the response.create (sent just before + // it) so it's in context for the reply. Other tools: a bare create. + client.requestResponse(result.image ? { image: result.image } : undefined); + return result; +} + +/** @param {string} query @returns {Promise} */ +async function execWebSearch(query) { + if (!query) return "No query provided."; + /** @type {Record} */ + const body = { query }; + // Only send a user key when there's no server key (server prefers its own). + if (!serverSearchKey && userSearchKey) body.key = userSearchKey; + + const res = await fetch("api/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + let detail = String(res.status); + try { const j = await res.json(); if (j.detail) detail = j.detail; } catch {} + throw new Error(`search error (${detail})`); + } + const json = await res.json(); + // Date-stamp the header so the model treats these as fresh realtime facts + // rather than its (older) training knowledge. + const today = new Date().toISOString().slice(0, 10); + /** @type {string[]} */ + const lines = [`Google search result from ${today}:`]; + if (json.answer) lines.push(`Answer: ${json.answer}`); + for (const r of json.results || []) { + lines.push(`- ${r.title}: ${r.snippet} (${r.url})`); + } + return lines.length > 1 ? lines.join("\n") : `${lines[0]}\nNo results found.`; +} + +/** Learn server config (search key + connection target), then refresh the UI. */ +async function fetchConfig() { + try { + const res = await fetch("api/config"); + if (res.ok) { + const json = await res.json(); + serverSearchKey = !!json.search; + lbMode = !!json.lb; + // Lock to LB mode only when the deploy reports a load balancer. + allowDirect = json.allowDirect ?? !lbMode; + // Deploy-pinned direct URL (overrides the LB server-side already). + pinnedUrl = (json.s2sUrl || "").trim(); + // The conversation-time limiter rides on the LB being present. + limiterOn = lbMode; + } + // Non-OK response: leave the fail-open default (allowDirect = true). + } catch { + // Config endpoint unreachable (e.g. static hosting): keep direct entry. + } + if (DEBUG) console.debug(`[ui] config: allowDirect=${allowDirect} lbMode=${lbMode}`); + // Login chip + remaining-budget (no-op / hidden when the limiter is off). + void account.refresh(); + syncToolsUi(); + syncConnectionUi(); +} + +/** + * Resolve where to connect, per the deploy's mode: + * • LB mode -> `{ sessionUrl }`, the client POSTs the same-origin /api/session + * proxy and the server forwards to the LB (its address stays server-side). + * • direct -> `{ directUrl }`, connect straight to the s2s WebSocket. + * Throws a user-facing error if direct mode is on but no URL was entered. + * @returns {{ sessionUrl: string } | { directUrl: string }} + */ +function connectionTarget() { + if (!allowDirect) { + return { sessionUrl: "api/session" }; + } + const directUrl = buildDirectWsUrl(pinnedUrl || settings.directUrl); + if (!directUrl) { + throw new Error("Enter a speech-to-speech server URL in Settings."); + } + return { directUrl }; +} + +/** + * Normalise a user-typed server address into a realtime WebSocket URL. + * Accepts bare hosts (`localhost:8080`), http(s) URLs, or ws(s) URLs, and adds + * the `/v1/realtime` path when none is given. A full connect URL (with path + * and/or query) is preserved as-is. + * @param {string} raw @returns {string} + */ +function buildDirectWsUrl(raw) { + let s = (raw || "").trim(); + if (!s) return ""; + if (!/^wss?:\/\//i.test(s)) { + if (/^https?:\/\//i.test(s)) { + s = s.replace(/^http/i, "ws"); // http→ws, https→wss + } else { + const isLocal = /^(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(s); + s = (isLocal ? "ws://" : "wss://") + s; + } + } + try { + const u = new URL(s); + if (u.pathname === "" || u.pathname === "/") u.pathname = "/v1/realtime"; + return u.toString(); + } catch { + return s; + } +} + +/** Create + resume an AudioContext synchronously (must run inside the user + * gesture so iOS lets it start). Returns null if construction fails. */ +function createResumedAudioContext() { + try { + const Ctx = window.AudioContext || /** @type {any} */ (window).webkitAudioContext; + const ctx = new Ctx({ latencyHint: "interactive" }); + if (ctx.state === "suspended") void ctx.resume().catch(() => {}); + return /** @type {AudioContext} */ (ctx); + } catch (err) { + console.warn("[main] AudioContext init failed:", err); + return null; + } +} + +/** Read the editable settings out of the form. The URL field is only honoured + * in free direct mode — in LB mode it's hidden, and when the deploy pins a + * URL it's read-only, so the user's saved URL survives either way. */ +function readSettingsFromForm() { + return { + directUrl: allowDirect && !pinnedUrl ? inputLbUrl.value.trim() : settings.directUrl, + voice: inputVoice.value || DEFAULT_VOICE, + instructions: inputInstructions.value.trim() || DEFAULT_INSTRUCTIONS, + noiseGate: readGateThreshold(), + }; +} + +/** Gate threshold (dBFS) currently shown on the slider, clamped to range. */ +function readGateThreshold() { + const v = Math.round(Number(inputNoiseGate.value)); + if (!Number.isFinite(v)) return GATE_OFF_DB; + return Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, v)); +} + +/** Adapt the connection field to the mode learned from /api/config. */ +function syncConnectionUi() { + if (pinnedUrl) { + // Deploy-pinned URL: show it, but locked — the deployment owns it. + connField.hidden = false; + inputLbUrl.value = pinnedUrl; + inputLbUrl.readOnly = true; + connHint.classList.remove("error"); + connHint.textContent = "Speech-to-speech server URL pinned by this deployment."; + } else if (allowDirect) { + // Direct mode: the user sets their own s2s server URL. + connField.hidden = false; + inputLbUrl.value = settings.directUrl; + inputLbUrl.readOnly = false; + inputLbUrl.placeholder = "http://localhost:port"; + connHint.classList.remove("error"); + connHint.textContent = + "URL of your speech-to-speech server, e.g. http://localhost:8080 (the app adds /v1/realtime)."; + } else { + // LB mode: the load balancer URL is deployment-owned — hide it entirely so + // its address is never exposed in Settings. + connField.hidden = true; + } +} + +/** True when the user must supply a server URL before connecting (direct mode + * with nothing set). */ +function missingServerUrl() { + return allowDirect && !pinnedUrl && !buildDirectWsUrl(settings.directUrl); +} + +/** Open Settings and point the user at the empty server-URL field. */ +function promptServerUrl() { + if (settingsModal.open) syncConnectionUi(); + else openSettings(); + connHint.textContent = "Set the speech-to-speech server URL to start."; + connHint.classList.add("error"); + inputLbUrl.focus(); +} + +settingsForm.addEventListener("submit", (event) => { + const submitter = /** @type {HTMLButtonElement | null} */ ((/** @type {SubmitEvent} */ (event)).submitter); + if (submitter?.value !== "save") return; + + settings = readSettingsFromForm(); + saveSettings(settings); + + // Voice + instructions can apply to a live session without reconnecting; a + // changed connection URL only takes effect on the next restart. + if (client && LIVE_STATES.has(currentState)) { + client.updateSession({ voice: settings.voice, instructions: effectiveInstructions() }); + } +}); + +// The noise gate applies live (worklet param), so tune it without a restart: +// update the label/marker, persist, and push straight to the running client. +inputNoiseGate.addEventListener("input", () => { + setGateThreshold(readGateThreshold()); +}); + +restartBtn.addEventListener("click", async () => { + if (currentState === "connecting") return; // a connect is already underway + settings = readSettingsFromForm(); + saveSettings(settings); + if (missingServerUrl()) { promptServerUrl(); return; } // keep settings open + settingsModal.close(); + // Grab the AudioContext NOW, inside the click gesture — teardown() awaits, and + // creating it afterwards would fall outside the gesture (silent on iOS). + const audioContext = createResumedAudioContext(); + try { + if (client) await teardown(); + await doStart(audioContext); + } catch (err) { + await handleStartError(err); + } +}); + +circleBtn.addEventListener("click", async () => { + try { + if (currentState === "idle" || currentState === "error") { + if (missingServerUrl()) { promptServerUrl(); return; } + await doStart(); + } + } catch (err) { + await handleStartError(err); + } +}); + +/** A failed start is either the daily limit (show the modal, return to idle) or + * a real fault (surface it). doStart already closed any orphan AudioContext. + * @param {any} err */ +async function handleStartError(err) { + if (err && err.code === "limit") { + await teardown(); + account.showLimit(err.tier); + return; + } + // The user left the queue (close() aborted the wait): teardown already reset + // the UI to idle, so there's nothing to report. + if (err && err.code === "aborted") return; + // The whole waiting line is full: a warm, reassuring modal rather than an error. + if (err && err.code === "queue-full") { + await teardown(); + account.showBusy(); + return; + } + // Our place lapsed (ticket reaped, or the join window ran out). Recoverable, not + // a fault: land on the retry state with a kind, plain-language reason. + if (err && (err.code === "queue-expired" || err.code === "join-expired")) { + await teardown(); + setState("error"); + setCaption( + err.code === "join-expired" + ? "Your spot expired. Tap to rejoin." + : "That took a while. Tap to rejoin.", + "error", + ); + return; + } + onFatalError(err); +} + +micBtn.addEventListener("click", () => { + if (!micStream || !client) return; + micMuted = !micMuted; + for (const track of micStream.getAudioTracks()) { + track.enabled = !micMuted; + } + client.setMuted(micMuted); + micBtn.classList.toggle("muted", micMuted); + micBtn.setAttribute("aria-label", micMuted ? "Unmute" : "Mute"); + micBtn.title = micMuted ? "Unmute" : "Mute"; +}); + +stopBtn.addEventListener("click", async () => { + await teardown(); +}); + +// "Leave queue": tear down the pending connect (aborts the poll wait) and drop +// our place in line. Same teardown path as stopping a live call. +leaveQueueBtn.addEventListener("click", async () => { + await teardown(); +}); + +// "Join now": accept the held slot. The click is a user gesture, so the client +// re-resumes the AudioContext here (iOS) before dialing. +joinQueueBtn.addEventListener("click", () => { + stopJoinCountdown(); + if (client) client.join(); +}); + +const MIC_CONSTRAINTS = { + audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, +}; + +/** Prompt for mic permission up front, then immediately release the tracks so no + * recording indicator lingers during a queue wait. Throws a friendly error if the + * user denies. */ +async function primeMicPermission() { + try { + const s = await navigator.mediaDevices.getUserMedia(MIC_CONSTRAINTS); + for (const track of s.getTracks()) track.stop(); + } catch (err) { + throw new Error( + `Microphone access denied${err instanceof Error ? `: ${err.message}` : ""}`, + ); + } +} + +/** Acquire the live capture stream once a slot is granted. Permission was primed + * in the tap gesture, so this is silent. Stored module-side for mute + teardown. */ +async function acquireMicStream() { + micStream = await navigator.mediaDevices.getUserMedia(MIC_CONSTRAINTS); + return micStream; +} + +/** @param {number} position Update the queued caption ("You're #N in line"). */ +function onQueuePosition(position) { + const n = Number(position) || 0; + setCaption(n > 0 ? `You're #${n} in line` : "Finding you a spot…", "muted"); +} + +// ── "Your turn" join countdown ────────────────────────────────────────────── +// While a slot is held for us, show how long is left to accept it. The client's +// join gate expires just before the load balancer reclaims the slot. +let joinCountdownTimer = 0; + +/** @param {number} sec */ +function startJoinCountdown(sec) { + stopJoinCountdown(); + let left = Math.max(0, Math.floor(sec)); + const paint = () => { + joinQueueBtn.textContent = left > 0 ? `Join now (${left}s)` : "Join now"; + }; + paint(); + joinCountdownTimer = window.setInterval(() => { + left -= 1; + if (left <= 0) { + stopJoinCountdown(); + joinQueueBtn.textContent = "Join now"; + return; + } + paint(); + }, 1000); +} + +function stopJoinCountdown() { + if (joinCountdownTimer) { + clearInterval(joinCountdownTimer); + joinCountdownTimer = 0; + } +} + +/** + * Start a conversation. Pass a pre-created AudioContext when the caller already + * made one inside the tap/click gesture (required on iOS); otherwise one is + * created here, which is still inside the gesture for a direct orb tap. + * @param {AudioContext | null} [audioContext] + */ +async function doStart(audioContext = null) { + // Resolve the target before touching mic/audio so a misconfiguration (e.g. + // direct mode with no URL) fails fast with a clear message. + const target = connectionTarget(); + + chat.clear(); + chat.reset(); + setState("connecting"); + setCaption("Asking for mic…", "muted"); + + // Create + resume the AudioContext SYNCHRONOUSLY, still inside the gesture. + // iOS Safari only starts an AudioContext from a user gesture; if we waited + // until after the getUserMedia / session-creation awaits below, it would stay + // suspended and the whole pipeline would be silent. + if (!audioContext) audioContext = createResumedAudioContext(); + + // Prime the mic permission now (get the prompt out of the way up front), then + // release it. The real capture stream is acquired only once a slot is granted + // (see acquireMicStream), so the mic 'in use' indicator never lights while we + // sit in the queue. Permission persists, so the later acquire is silent. + try { + await primeMicPermission(); + } catch (err) { + if (audioContext) void audioContext.close().catch(() => {}); + throw err; + } + + // The webcam is started on arrival (autoStartCamera), so nothing to do here; + // a still-pending grant just means the snapshot tool isn't ready yet. + + const c = new S2sWsRealtimeClient({ + ...target, + voice: settings.voice, + instructions: effectiveInstructions(), + acquireMic: acquireMicStream, + tools: activeToolDefs(), + noiseGate: gateParams(settings.noiseGate), + ...(audioContext ? { audioContext } : {}), + }); + client = c; + + c.addEventListener("queue", (e) => { + const { position, queueId } = /** @type {CustomEvent<{ position: number; queueId: string }>} */ (e).detail; + if (queueId) queuedTicketId = queueId; + onQueuePosition(position); + }); + + c.addEventListener("ready-to-join", (e) => { + const { info, expiresSec } = /** @type {CustomEvent<{ info: import("./ws/s2s-ws-client.js").WsSessionInfo; expiresSec: number }>} */ (e).detail; + // A slot is held for us. We're out of the queue now, so drop the ticket ref. + // Track the granted session id already so that leaving (or letting the timer + // lapse) refunds the budget the server reserved at claim, even before we dial. + queuedTicketId = ""; + if (info?.sessionId) { + trackedSessionId = info.sessionId; + trackedTier = info.tier || "anon"; + } + startJoinCountdown(expiresSec); + }); + + c.addEventListener("status", (e) => { + const detail = /** @type {CustomEvent<{ status: string }>} */ (e).detail; + onClientStatus(detail.status); + }); + c.addEventListener("transcript", (e) => { + const d = /** @type {CustomEvent<{ role: "user" | "assistant"; text: string; partial: boolean; itemId?: string; responseId?: string }>} */ (e).detail; + chat.onTranscript(d); + }); + + c.addEventListener("response-finished", (e) => { + const detail = /** @type {CustomEvent<{ responseId: string; status: string; audible?: boolean; transcript?: string }>} */ (e).detail; + chat.onResponseFinished(detail); + }); + + c.addEventListener("toolcall", (e) => { + const { name, arguments: args, callId } = /** @type {CustomEvent<{ name: string; arguments: string; callId: string }>} */ (e).detail; + chat.onToolCall(name); + // Execute the tool, then push it to the conversation once the result is in, + // so the toggle shows both the call input and its output together. + void runTool(name, args, callId).then(({ output, image }) => { + chat.onToolResult(name, args, output, image); + }); + }); + c.addEventListener("error", (e) => { + const detail = /** @type {CustomEvent<{ error: unknown }>} */ (e).detail; + onFatalError(detail.error); + }); + c.addEventListener("server-error", (e) => { + // Non-fatal: the backend reported an error mid-session. Log it, keep the + // socket and the conversation alive (the model can recover on its own). + const detail = /** @type {CustomEvent<{ error: unknown }>} */ (e).detail; + const msg = detail.error instanceof Error ? detail.error.message : String(detail.error); + console.warn("[main] server error (non-fatal):", msg); + }); + c.addEventListener("session", (e) => { + const info = /** @type {CustomEvent<{ info: import("./ws/s2s-ws-client.js").WsSessionInfo }>} */ (e).detail.info; + console.log("[ws] session created:", info.sessionId); + // A slot was granted — we're out of the queue; drop the ticket reference so + // teardown doesn't try to leave a line we already left. + queuedTicketId = ""; + // A metered tier (anon / free): heartbeat so the server can extend the + // reservation and tell us when the daily budget runs out. PRO isn't limited. + if (info.limited && info.sessionId) { + trackedSessionId = info.sessionId; + trackedTier = info.tier || "anon"; + startHeartbeat(info.heartbeatSec || 5); + } + }); + c.addEventListener("input-level", (e) => { + const { rms } = /** @type {CustomEvent<{ rms: number }>} */ (e).detail; + paintInputLevel(rms); + }); + + try { + await c.connect(); + } catch (err) { + // The grant can be refused (402 → limit) or the dial can fail. In LB mode + // the AudioContext hasn't been adopted by the client yet (the session POST + // runs first), so close the one we created here to avoid leaking it. + if (audioContext) void audioContext.close().catch(() => {}); + throw err; + } +} + +// ── Conversation-time heartbeat ───────────────────────────────────────────── + +/** Ping the server every `sec` seconds so it can meter the live session; when + * it reports the daily budget is spent, cut the call and show the limit modal. + * @param {number} sec */ +function startHeartbeat(sec) { + stopHeartbeat(); + heartbeatTimer = window.setInterval(async () => { + if (!trackedSessionId) return; + try { + const res = await fetch("api/session/heartbeat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId: trackedSessionId }), + keepalive: true, + }); + const json = await res.json().catch(() => ({})); + if (json.expired) await onLimitReached(); + } catch (err) { + // A transient network blip shouldn't kill the call; the next tick retries. + if (DEBUG) console.debug("[ui] heartbeat failed:", err); + } + }, Math.max(1, sec) * 1000); +} + +function stopHeartbeat() { + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = 0; + } +} + +/** The server cut the live session: tear down and explain why. */ +async function onLimitReached() { + const tier = trackedTier; + stopHeartbeat(); + await teardown(); + account.showLimit(tier); +} + +/** Tell the server a session ended so it reconciles + refunds the unused chunk. + * Uses sendBeacon so it still fires when the tab is closing. */ +function endTrackedSession() { + if (!trackedSessionId) return; + const body = JSON.stringify({ sessionId: trackedSessionId }); + try { + const blob = new Blob([body], { type: "application/json" }); + if (!navigator.sendBeacon("api/session/end", blob)) { + void fetch("api/session/end", { + method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true, + }).catch(() => {}); + } + } catch { + // Best-effort; the server sweep reaps the session anyway. + } + trackedSessionId = ""; + trackedTier = ""; +} + +/** Leave the waiting queue so the LB frees our place. sendBeacon so it still + * fires on tab close; the LB also reaps the ticket on TTL as a backstop. */ +function endQueueTicket() { + if (!queuedTicketId) return; + const body = JSON.stringify({ queueId: queuedTicketId }); + try { + const blob = new Blob([body], { type: "application/json" }); + if (!navigator.sendBeacon("api/queue/end", blob)) { + void fetch("api/queue/end", { + method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true, + }).catch(() => {}); + } + } catch { + // Best-effort; the LB reaps the ticket on TTL anyway. + } + queuedTicketId = ""; +} + +/** @param {string} status */ +function onClientStatus(status) { + switch (status) { + case "creating-session": + case "connecting": + setState("connecting"); + break; + case "queued": + setState("queued"); + break; + case "your-turn": + setState("your-turn"); + break; + case "connected": + setState("listening"); + break; + case "user-speaking": + setState("user-speaking"); + break; + case "processing": + setState("processing"); + break; + case "ai-speaking": + setState("ai-speaking"); + break; + case "closed": + // teardown() will move us to idle + break; + case "error": + setState("error"); + break; + } +} + +async function teardown() { + stopHeartbeat(); + stopJoinCountdown(); + endTrackedSession(); + endQueueTicket(); + chat.reset({ dismiss: true }); + if (client) { + try { + await client.close(); + } catch (err) { + console.warn("[main] error closing client:", err); + } + client = null; + } + if (micStream) { + for (const track of micStream.getTracks()) track.stop(); + micStream = null; + } + // The webcam is independent of the call lifecycle (it runs while the user is + // on the page), so we leave it on here — only the camera toggle stops it. + micMuted = false; + micBtn.classList.remove("muted"); + setState("idle"); + // Refresh the chip's remaining-today after the budget moved. + if (limiterOn) void account.refresh(); +} + +/** @param {unknown} err */ +function onFatalError(err) { + console.error("[main] fatal:", err); + setState("error"); + const message = err instanceof Error ? err.message : String(err); + setCaption(truncateError(message), "error"); + void teardown().catch(() => { + setState("error"); + setCaption(truncateError(message), "error"); + }); +} + +setState("idle"); +chat.renderEmptyState(); +initGateArc(); +void fetchConfig(); + +// ── Text input (set up early, before camera/permission calls) ─────────────── +try { + const textInput = /** @type {HTMLInputElement} */ ($("#text-input")); + const sendBtn = /** @type {HTMLButtonElement} */ ($("#text-send-btn")); + if (textInput && sendBtn) { + const doSendText = (/** @type {string} */ text) => { + if (!text) return; + if (!client) { + setCaption("Start a conversation first", ""); + return; + } + client.sendUserText(text); + chat.onTranscript({ role: "user", text, partial: false }); + textInput.value = ""; + }; + const onTextSend = () => doSendText(textInput.value.trim()); + textInput.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + onTextSend(); + } + }); + sendBtn.addEventListener("click", onTextSend); + } else { + console.error("[main] text input elements not found"); + } +} catch (err) { + console.error("[main] text input setup failed:", err); +} + +// Start the webcam as soon as the user lands (camera tool defaults on), and +// react to later permission changes (re-grant after a denial re-enables it). +void autoStartCamera(); +void watchCameraPermission(); + +// Reconcile a live session if the tab is closed/hidden mid-call (no teardown). +window.addEventListener("pagehide", () => { endTrackedSession(); endQueueTicket(); }); + +requestAnimationFrame(() => { + document.body.classList.remove("booting"); +}); + +// ── Audio debug overlay (fixed bottom-right) ──────────────────────────────── +(() => { + const el = document.createElement("div"); + el.id = "audio-debug"; + el.style.cssText = + "position:fixed;bottom:8px;right:8px;z-index:9999;font:11px/1.4 monospace;" + + "color:#9fe8a0;background:rgba(0,0,0,.65);padding:6px 8px;border-radius:6px;" + + "pointer-events:none;white-space:pre;"; + document.body.appendChild(el); + const WS_READY = { 0: "CONNECTING", 1: "OPEN", 2: "CLOSING", 3: "CLOSED" }; + setInterval(() => { + if (!client) { el.textContent = "audio: (not started)"; return; } + const d = client.getAudioDebug(); + el.textContent = + "ws=" + (WS_READY[d.ws] ?? d.ws) + " ctx=" + d.ctx + + "\ndeltas=" + d.deltas + " → wl=" + d.received + + "\npeak=" + (d.peak.toFixed ? d.peak.toFixed(3) : d.peak) + + "\nplayed=" + d.played; + }, 1000); +})(); diff --git a/backend/vendor/s2s-cloud/demo/main.js.orig b/backend/vendor/s2s-cloud/demo/main.js.orig new file mode 100644 index 0000000..da07219 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/main.js.orig @@ -0,0 +1,1440 @@ +// @ts-check +/** + * Minimal voice conversation app, talking to a Hugging Face speech-to-speech + * backend over **WebSocket** (drop-in alternative to the WebRTC variant). + * + * Click the orb -> we ask for the mic, POST a session on the LB, open a + * WebSocket on the routed compute endpoint, push session.update + mic + * audio, play back the TTS audio. The orb visually reflects the live + * state (idle, connecting, listening, user-speaking, processing, + * ai-speaking). + * + * The only meaningful difference vs. the WebRTC main.js is that the + * client owns its own AudioContext (no `attachOutputTrack`), so we hand + * it the MediaStream directly. + * + * @typedef {"idle" | "connecting" | "queued" | "your-turn" | "listening" | "user-speaking" | "processing" | "ai-speaking" | "error"} AppState + */ + +import { S2sWsRealtimeClient } from "./ws/s2s-ws-client.js"; +import { $, truncateError, DEBUG } from "./ui/dom.js"; +import { ChatView } from "./ui/chat.js"; +import { Account } from "./ui/account.js"; + +const DEFAULT_VOICE = "Aiden"; +const DEFAULT_INSTRUCTIONS = + "You are a friendly voice assistant. " + + "Keep replies short, warm, and spoken. Avoid long monologues."; + +// Appended to the user's instructions whenever at least one tool is enabled. +// Stops the model from announcing capabilities ("Yes, I can search") and then +// idling for the next turn — it should act immediately in the same response. +const TOOL_USE_HINT = + " When the user's request calls for one of your tools, do not describe your " + + "capabilities or say you can do it and wait for another turn. Instead, say " + + 'a brief acknowledgement like "Let me search for that..." and call the tool ' + + "right away in the same response."; + +const STORAGE_KEYS = { + // Direct s2s server URL, used only when the deploy has no LOAD_BALANCER_URL + // (in LB mode the browser never learns the LB address — it POSTs /api/session). + directUrl: "s2s.ws.directUrl", + voice: "s2s.ws.voice", + instructions: "s2s.ws.instructions", + tools: "s2s.ws.tools", + searchKey: "s2s.ws.searchKey", + noiseGate: "s2s.ws.noiseGate", +}; + +// ── Noise gate ────────────────────────────────────────────────────────────── +// The Settings cursor sets the gate's open threshold in dBFS. Its leftmost +// position is an OFF detent (gate disabled, pure passthrough); the rest of the +// travel is the active threshold. The cursor shares the meter's dB axis, so the +// handle sits on the level bar — raise it until room noise stops lighting it up. +// The slider range IS the shared axis: the live meter fill and the threshold +// thumb both map across [GATE_OFF_DB, GATE_MAX_DB], so the thumb sits exactly +// where the gate cuts on the same scale as the level bar. +const GATE_OFF_DB = -66; // slider minimum = off / bottom of the meter axis +const GATE_MAX_DB = -3; // slider maximum = most aggressive / top of the meter axis +const GATE_DEFAULT_DB = -50; // first-run default: a gentle gate, enabled + +/** @param {number} thresholdDb @returns {import("./ws/s2s-ws-client.js").NoiseGate} */ +function gateParams(thresholdDb) { + return { enabled: thresholdDb > GATE_OFF_DB, thresholdDb }; +} + +// ── Tools ───────────────────────────────────────────────────────────────── +// Function tools we declare to the backend. The model decides when to call +// one; the executor below runs it and returns the result (see runTool). +/** @type {Record} */ +const TOOL_DEFS = { + web_search: { + type: "function", + name: "web_search", + description: + "Search the web for current or factual information you don't already know " + + "(news, prices, facts, documentation). Returns the top results with titles, " + + "snippets and URLs.", + parameters: { + type: "object", + properties: { query: { type: "string", description: "The search query." } }, + required: ["query"], + }, + }, + camera_snapshot: { + type: "function", + name: "camera_snapshot", + description: + "Capture the current frame from the user's webcam so you can see what they " + + "are showing you. Use it whenever the user refers to something visual or " + + "asks you to look.", + parameters: { type: "object", properties: {}, required: [] }, + }, +}; + +/** Longest edge of the snapshot sent to the VLM, in px (keeps payload sane). */ +const SNAPSHOT_MAX_EDGE = 768; +const SNAPSHOT_QUALITY = 0.7; + +function loadSettings() { + return { + directUrl: localStorage.getItem(STORAGE_KEYS.directUrl) || "", + voice: localStorage.getItem(STORAGE_KEYS.voice) || DEFAULT_VOICE, + instructions: localStorage.getItem(STORAGE_KEYS.instructions) || DEFAULT_INSTRUCTIONS, + noiseGate: loadGateThreshold(), + }; +} + +/** Stored gate threshold (dBFS), clamped to the slider range. Defaults to a + * gentle enabled gate (GATE_DEFAULT_DB) when the user hasn't set one yet. */ +function loadGateThreshold() { + const stored = localStorage.getItem(STORAGE_KEYS.noiseGate); + // getItem returns null when unset, and Number(null) === 0 (finite!), so guard + // the missing/empty case explicitly before coercing — otherwise the default + // never fires and 0 clamps to the slider max. + if (stored === null || stored === "") return GATE_DEFAULT_DB; + const raw = Number(stored); + if (!Number.isFinite(raw)) return GATE_DEFAULT_DB; + return Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, Math.round(raw))); +} + +/** @param {ReturnType} s */ +function saveSettings(s) { + localStorage.setItem(STORAGE_KEYS.directUrl, s.directUrl); + localStorage.setItem(STORAGE_KEYS.voice, s.voice); + localStorage.setItem(STORAGE_KEYS.instructions, s.instructions); + localStorage.setItem(STORAGE_KEYS.noiseGate, String(s.noiseGate)); +} + +/** @returns {{ web_search: boolean, camera_snapshot: boolean }} */ +function loadTools() { + try { + const raw = JSON.parse(localStorage.getItem(STORAGE_KEYS.tools) || "{}"); + // Both tools default ON (web search still only activates when a key exists). + // We never call getUserMedia on page load — the camera only actually starts + // on a user gesture (conversation start), so a default-on flag doesn't + // silently resume the webcam; an explicit saved `false` is respected. + return { + web_search: raw.web_search ?? true, + camera_snapshot: raw.camera_snapshot ?? true, + }; + } catch { + return { web_search: true, camera_snapshot: true }; + } +} + +function saveTools() { + localStorage.setItem(STORAGE_KEYS.tools, JSON.stringify(toolsEnabled)); +} + +/** @type {Record} */ +const STATE_VIEWS = { + idle: { caption: "Tap to start", disabled: false }, + connecting: { caption: "Connecting", disabled: true }, + queued: { caption: "Finding you a spot…", disabled: true }, + "your-turn": { caption: "You're up! 🎉", disabled: true }, + listening: { caption: "", disabled: false }, + "user-speaking": { caption: "", disabled: false }, + processing: { caption: "", disabled: false }, + "ai-speaking": { caption: "", disabled: false }, + error: { caption: "Tap to retry", disabled: false }, +}; + +/** @type {Record} */ +const STATE_CLASS = { + idle: "state-idle", + connecting: "state-connecting", + queued: "state-queued", + "your-turn": "state-your-turn", + listening: "state-listening", + "user-speaking": "state-user-speaking", + processing: "state-processing", + "ai-speaking": "state-ai-speaking", + error: "state-error", +}; + +/** @type {ReadonlySet} */ +const LIVE_STATES = new Set(["listening", "user-speaking", "processing", "ai-speaking"]); + +/** @type {HTMLButtonElement} */ +const circleBtn = $("#main-circle"); +/** @type {HTMLParagraphElement} */ +const circleCaption = $("#circle-caption"); +/** @type {HTMLParagraphElement} */ +const circleSubcaption = $("#circle-subcaption"); +/** @type {HTMLElement} */ +const orbWrap = $(".orb-wrap"); +/** @type {HTMLButtonElement} */ +const micBtn = $("#mic-btn"); +/** @type {HTMLButtonElement} */ +const stopBtn = $("#stop-btn"); +/** @type {HTMLElement} */ +const queueActions = $("#queue-actions"); +/** @type {HTMLButtonElement} */ +const joinQueueBtn = $("#join-queue-btn"); +/** @type {HTMLButtonElement} */ +const leaveQueueBtn = $("#leave-queue-btn"); + +/** @type {HTMLButtonElement} */ +const settingsBtn = $("#settings-btn"); +/** @type {HTMLDialogElement} */ +const settingsModal = $("#settings-modal"); + +/** @type {HTMLButtonElement} */ +const aboutBtn = $("#about-btn"); +/** @type {HTMLDialogElement} */ +const aboutModal = $("#about-modal"); +/** @type {HTMLButtonElement} */ +const aboutClose = $("#about-close"); + +/** @type {HTMLButtonElement} */ +const toolsBtn = $("#tools-btn"); +/** @type {HTMLDialogElement} */ +const toolsModal = $("#tools-modal"); +/** @type {HTMLButtonElement} */ +const toolsClose = $("#tools-close"); +/** @type {HTMLInputElement} */ +const toolWebSwitch = $("#tool-web"); +/** @type {HTMLInputElement} */ +const toolCamSwitch = $("#tool-cam"); +/** @type {HTMLElement} */ +const toolWebRow = $("#tool-web-row"); +/** @type {HTMLElement} */ +const toolWebHint = $("#tool-web-hint"); +/** @type {HTMLElement} */ +const toolCamHint = $("#tool-cam-hint"); +/** @type {HTMLInputElement} */ +const searchKeyInput = $("#search-key"); +/** @type {HTMLElement} */ +const camPip = $("#cam-pip"); +/** @type {HTMLVideoElement} */ +const camVideo = $("#cam-video"); + +/** @type {HTMLInputElement} */ +const inputLbUrl = $("#lb-url"); +/** @type {HTMLElement} */ +const connField = $("#conn-field"); +/** @type {HTMLElement} */ +const connHint = $("#conn-hint"); +/** @type {HTMLSelectElement} */ +const inputVoice = $("#voice"); +/** @type {HTMLTextAreaElement} */ +const inputInstructions = $("#instructions"); +/** @type {HTMLInputElement} */ +const inputNoiseGate = $("#noise-gate"); +/** @type {HTMLElement} */ +const gateValue = $("#gate-value"); +/** @type {HTMLElement} */ +const gateMeterFill = $("#gate-meter-fill"); +/** @type {HTMLElement} */ +const micGate = $("#mic-gate"); +const mgaArc = /** @type {SVGSVGElement} */ (document.querySelector("#mic-gate-arc")); +const mgaTrack = /** @type {SVGPathElement} */ (document.querySelector("#mga-track")); +const mgaFill = /** @type {SVGPathElement} */ (document.querySelector("#mga-fill")); +const mgaHit = /** @type {SVGPathElement} */ (document.querySelector("#mga-hit")); +const mgaHandle = /** @type {SVGCircleElement} */ (document.querySelector("#mga-handle")); +/** @type {HTMLButtonElement} */ +const restartBtn = $("#restart-conversation"); +/** @type {HTMLElement} */ +const restartHint = $("#restart-hint"); +const settingsForm = /** @type {HTMLFormElement} */ (settingsModal.querySelector("form")); + +/** @type {AppState} */ +let currentState = "idle"; +let settings = loadSettings(); + +// ── Connection target ──────────────────────────────────────────────────────── +// Three modes, decided by the deploy via /api/config: +// • SPEECH_TO_SPEECH_URL set -> direct mode pinned by the deploy: the browser +// connects straight to that URL, shown read-only in Settings. Overrides the +// load balancer entirely. +// • LOAD_BALANCER_URL set -> original flow: POST the same-origin /api/session +// proxy (the server forwards to the LB; the LB address is never sent here). +// • neither (allowDirect) -> the user sets a speech-to-speech server URL and +// the browser connects to it directly (no load balancer, no /session). +let lbMode = false; +// Fail open: direct entry is allowed unless /api/config reports an LB URL. This +// way a missing/unreachable config (e.g. static hosting) leaves the field +// usable rather than locked. +let allowDirect = true; +// Deploy-pinned s2s URL (SPEECH_TO_SPEECH_URL). Non-empty -> locked direct +// mode: the field displays it read-only and the saved user URL is untouched. +let pinnedUrl = ""; + +// ── Tool state ────────────────────────────────────────────────────────────── +let toolsEnabled = loadTools(); +// Whether the server holds a Serper key (learned from /api/config on load). +let serverSearchKey = false; +// A user-supplied key (fallback when the deploy has none). localStorage only. +let userSearchKey = localStorage.getItem(STORAGE_KEYS.searchKey) || ""; +/** @type {MediaStream | null} */ +let cameraStream = null; + +/** Search is usable if the server has a key or the user supplied one. */ +function searchAvailable() { + return serverSearchKey || !!userSearchKey; +} + +/** Tool definitions for the currently-enabled (and usable) tools. */ +function activeToolDefs() { + const defs = []; + if (toolsEnabled.web_search && searchAvailable()) defs.push(TOOL_DEFS.web_search); + if (toolsEnabled.camera_snapshot) defs.push(TOOL_DEFS.camera_snapshot); + return defs; +} + +/** Instructions plus the hidden tool-use hint when any tool is active. */ +function effectiveInstructions() { + const base = settings.instructions; + return activeToolDefs().length ? base + TOOL_USE_HINT : base; +} + +/** Push the active tool set to a live session so toggles apply mid-call. */ +function pushToolsToSession() { + if (!client || !LIVE_STATES.has(currentState)) return; + client.setTools(activeToolDefs()); + // The hidden tool-use hint depends on whether any tool is active, so refresh + // instructions alongside the tool set. + client.updateSession({ instructions: effectiveInstructions() }); +} + +// ── Chat view ─────────────────────────────────────────────────────────────── +// Owns the history panel, the ephemeral bubbles, and all transcript/tool +// streaming state. The client's events are forwarded to its on* methods. +const chat = new ChatView(); + +// ── Account / limiter ───────────────────────────────────────────────────── +// Login chip + daily-limit modal (inert unless the deploy is in LB mode). The +// server meters conversation time; the client just heartbeats a live session +// and tears down when the server reports the budget is spent. +const account = new Account(); +let limiterOn = false; +let heartbeatTimer = 0; +let trackedSessionId = ""; +let trackedTier = ""; +// The waiting-queue ticket id while we're in line (else ""). Used to leave the +// queue on teardown / tab-close so we don't hold a phantom place. +let queuedTicketId = ""; + +/** @type {S2sWsRealtimeClient | null} */ +let client = null; +/** @type {MediaStream | null} */ +let micStream = null; +let micMuted = false; + +/** @param {AppState} next */ +function setState(next) { + currentState = next; + const view = STATE_VIEWS[next]; + circleBtn.disabled = view.disabled; + circleBtn.className = `circle ${STATE_CLASS[next]}`; + if (next !== "error") setCaption(view.caption); + + const live = LIVE_STATES.has(next); + orbWrap.classList.toggle("live", live); + micBtn.setAttribute("aria-hidden", live ? "false" : "true"); + stopBtn.setAttribute("aria-hidden", live ? "false" : "true"); + micBtn.tabIndex = live ? 0 : -1; + stopBtn.tabIndex = live ? 0 : -1; + + // Queue affordances: "Leave queue" whenever we're in line; "Join now" only once + // it's our turn (a slot is held for us). Both live under #queue-actions. + const yourTurn = next === "your-turn"; + const inLine = next === "queued" || yourTurn; + queueActions.hidden = !inLine; + joinQueueBtn.hidden = !yourTurn; + joinQueueBtn.tabIndex = yourTurn ? 0 : -1; + leaveQueueBtn.hidden = !inLine; + leaveQueueBtn.tabIndex = inLine ? 0 : -1; + if (!yourTurn) stopJoinCountdown(); + + // Warm reassurance under the terse position, only while waiting in line. + if (next === "queued") { + circleSubcaption.textContent = + "Sorry, we overhugged! 🤗 Every slot is busy, so we saved you a spot. Hang tight, you're moving up."; + circleSubcaption.hidden = false; + } else { + circleSubcaption.hidden = true; + } + + updateRestartAvailability(); +} + +function updateRestartAvailability() { + // Restart works from any settled state — it tears down a live call (if any) + // and reconnects with the current settings. Only block while mid-connect or + // while waiting in the queue (restarting from there would just re-queue). + restartBtn.disabled = + currentState === "connecting" || currentState === "queued" || currentState === "your-turn"; + restartHint.hidden = false; + restartHint.textContent = LIVE_STATES.has(currentState) + ? "Reconnects now with the settings above." + : "Starts a conversation with the settings above."; +} + +/** + * @param {string} text + * @param {"" | "error" | "muted"} [kind] + */ +function setCaption(text, kind = "") { + const trimmed = text.trim(); + circleCaption.textContent = trimmed; + circleCaption.className = `circle-caption${kind ? ` ${kind}` : ""}${trimmed ? "" : " empty"}`; +} + +function openSettings() { + syncConnectionUi(); + inputVoice.value = settings.voice; + inputInstructions.value = settings.instructions; + syncGateUi(); + updateRestartAvailability(); + settingsModal.showModal(); +} + +/** dB position (clamped to the slider axis) as a 0..1 fraction of the track. + * @param {number} db */ +function dbToFraction(db) { + const clamped = Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, db)); + return (clamped - GATE_OFF_DB) / (GATE_MAX_DB - GATE_OFF_DB); +} + +/** @param {number} f @returns {number} dB at a 0..1 position on the gate axis. */ +function fractionToDb(f) { + const clamped = Math.min(1, Math.max(0, f)); + return Math.round(GATE_OFF_DB + clamped * (GATE_MAX_DB - GATE_OFF_DB)); +} + +// ── Radial gate arc (around the mic button, live during a call) ───────────── +// A 270° arc with the gap facing the orb (right). Fraction 0 (=Off) sits at the +// bottom-ish start; 1 (=max) at the top-ish end. The level fill and the +// threshold handle ride this same axis, mirroring the Settings widget. +const ARC_R = 40; +// A ~200° arc centred on the left (180°) so the wide gap faces the orb (right). +const ARC_SPAN_DEG = 200; +const ARC_START_DEG = 180 - ARC_SPAN_DEG / 2; // lower-left start; Off end + +/** Point at fraction f (0..1) and radius r, in the 0..100 viewBox. + * @param {number} f @param {number} [r] */ +function arcPoint(f, r = ARC_R) { + const deg = ARC_START_DEG + f * ARC_SPAN_DEG; + const rad = (deg * Math.PI) / 180; + return { x: 50 + r * Math.cos(rad), y: 50 + r * Math.sin(rad) }; +} + +/** SVG path `d` for the full 0..1 arc (clockwise). */ +function fullArcD() { + const a = arcPoint(0); + const b = arcPoint(1); + const largeArc = ARC_SPAN_DEG > 180 ? 1 : 0; + return `M ${a.x} ${a.y} A ${ARC_R} ${ARC_R} 0 ${largeArc} 1 ${b.x} ${b.y}`; +} + +/** One-time geometry: track, fill (dash-revealed) and the transparent hit band. */ +function initGateArc() { + const d = fullArcD(); + mgaTrack.setAttribute("d", d); + mgaFill.setAttribute("d", d); + mgaHit.setAttribute("d", d); + // pathLength 100 lets us reveal the fill by fraction via dashoffset. + mgaFill.setAttribute("pathLength", "100"); + mgaFill.style.strokeDasharray = "100 100"; + mgaFill.style.strokeDashoffset = "100"; // empty until levels arrive + renderGateHandle(); +} + +/** Place the threshold bead on the arc at the stored threshold; flag off state. */ +function renderGateHandle() { + const off = settings.noiseGate <= GATE_OFF_DB; + const p = arcPoint(dbToFraction(settings.noiseGate)); + mgaHandle.setAttribute("cx", String(p.x)); + mgaHandle.setAttribute("cy", String(p.y)); + micGate.classList.toggle("gate-off", off); +} + +/** Paint a 0..1 live level onto the arc fill (and the Settings meter if open). + * Brightens the tick when the level crosses the threshold — i.e. the gate is + * actually open — but only when gating is enabled. + * @param {number} rms */ +function paintInputLevel(rms) { + const db = rms > 0 ? 20 * Math.log10(rms) : GATE_OFF_DB; + const f = dbToFraction(db); + mgaFill.style.strokeDashoffset = String(100 * (1 - f)); + if (settingsModal.open) gateMeterFill.style.width = `${f * 100}%`; + const enabled = settings.noiseGate > GATE_OFF_DB; + micGate.classList.toggle("gate-open", enabled && f >= dbToFraction(settings.noiseGate)); +} + +/** The single place that commits a new gate threshold: updates both controls, + * persists, and applies live to the running session. + * @param {number} db */ +function setGateThreshold(db) { + settings.noiseGate = Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, Math.round(db))); + const off = settings.noiseGate <= GATE_OFF_DB; + inputNoiseGate.value = String(settings.noiseGate); + gateValue.textContent = off ? "Off" : `${settings.noiseGate} dB`; + renderGateHandle(); + localStorage.setItem(STORAGE_KEYS.noiseGate, String(settings.noiseGate)); + if (client && LIVE_STATES.has(currentState)) { + client.setNoiseGate(gateParams(settings.noiseGate)); + } +} + +/** Reflect the stored gate threshold into the slider, label and arc handle. */ +function syncGateUi() { + inputNoiseGate.value = String(settings.noiseGate); + const off = settings.noiseGate <= GATE_OFF_DB; + gateValue.textContent = off ? "Off" : `${settings.noiseGate} dB`; + renderGateHandle(); +} + +// Drag along the arc band to set the threshold (a tap on the glyph still mutes). +let gateDragging = false; +/** @param {PointerEvent} e */ +function gatePointerToDb(e) { + const rect = mgaArc.getBoundingClientRect(); + const cx = rect.left + rect.width / 2; + const cy = rect.top + rect.height / 2; + let deg = (Math.atan2(e.clientY - cy, e.clientX - cx) * 180) / Math.PI; + if (deg < 0) deg += 360; + // Map the on-arc angle to a fraction; angles in the right-side gap fall + // outside [0,1] and fractionToDb clamps them to the nearest end (just-below + // start -> Off, just-past end -> max). + const f = (deg - ARC_START_DEG) / ARC_SPAN_DEG; + return fractionToDb(f); +} +mgaHit.addEventListener("pointerdown", (e) => { + gateDragging = true; + mgaHit.setPointerCapture(e.pointerId); + setGateThreshold(gatePointerToDb(e)); +}); +mgaHit.addEventListener("pointermove", (e) => { + if (gateDragging) setGateThreshold(gatePointerToDb(e)); +}); +const endGateDrag = (/** @type {PointerEvent} */ e) => { + if (!gateDragging) return; + gateDragging = false; + try { mgaHit.releasePointerCapture(e.pointerId); } catch {} +}; +mgaHit.addEventListener("pointerup", endGateDrag); +mgaHit.addEventListener("pointercancel", endGateDrag); + +settingsBtn.addEventListener("click", openSettings); + +// About panel: native , Esc closes for free; also close on the X and +// on a click in the backdrop (a click whose target is the dialog itself). +aboutBtn.addEventListener("click", () => aboutModal.showModal()); +// Mobile twin of the (i), living in the right-hand control cluster. +$("#about-btn-m").addEventListener("click", () => aboutModal.showModal()); +aboutClose.addEventListener("click", () => aboutModal.close()); +aboutModal.addEventListener("click", (e) => { + if (e.target === aboutModal) aboutModal.close(); +}); + +// ── Tools panel ─────────────────────────────────────────────────────────── + +/** Reflect the current tool state into the panel controls. */ +function syncToolsUi() { + const avail = searchAvailable(); + toolWebSwitch.checked = toolsEnabled.web_search && avail; + toolWebSwitch.disabled = !avail; + toolWebRow.classList.toggle("disabled", !avail); + toolCamSwitch.checked = toolsEnabled.camera_snapshot; + + if (serverSearchKey) { + // Key lives server-side: show it as configured, never expose it. + searchKeyInput.value = ""; + searchKeyInput.placeholder = "•••••••• · provided by the server"; + searchKeyInput.disabled = true; + toolWebHint.textContent = "Ready. The search key is held server-side and never sent to your browser."; + } else { + searchKeyInput.disabled = false; + searchKeyInput.value = userSearchKey; + searchKeyInput.placeholder = "Paste a Serper key to enable web search"; + toolWebHint.textContent = userSearchKey + ? "Using your key — stored in this browser only." + : "No server key configured. Add your own Serper key to enable web search."; + } +} + +toolsBtn.addEventListener("click", () => { syncToolsUi(); toolsModal.showModal(); }); +toolsClose.addEventListener("click", () => toolsModal.close()); +toolsModal.addEventListener("click", (e) => { + if (e.target === toolsModal) toolsModal.close(); +}); + +toolWebSwitch.addEventListener("change", () => { + if (toolWebSwitch.checked && !searchAvailable()) { + toolWebSwitch.checked = false; // guard: can't enable without a key + return; + } + toolsEnabled.web_search = toolWebSwitch.checked; + saveTools(); + pushToolsToSession(); +}); + +toolCamSwitch.addEventListener("change", async () => { + if (toolCamSwitch.checked) { + try { + // Flipping the switch always re-requests the camera, so a permission that + // was only dismissed earlier is asked again here. + await enableCamera(); + } catch (err) { + toolCamSwitch.checked = false; + const denied = err instanceof Error && (err.name === "NotAllowedError" || err.name === "SecurityError"); + toolCamHint.textContent = denied + ? "Camera blocked. Allow it from the camera icon in your browser's address bar — it switches on automatically." + : `Camera unavailable${err instanceof Error ? `: ${err.message}` : ""}`; + return; + } + toolsEnabled.camera_snapshot = true; + toolCamHint.textContent = "Camera on. The assistant can take a snapshot when it needs to see."; + } else { + disableCamera(); + toolsEnabled.camera_snapshot = false; + toolCamHint.textContent = "Let the assistant see through your webcam."; + } + saveTools(); + pushToolsToSession(); +}); + +searchKeyInput.addEventListener("input", () => { + if (serverSearchKey) return; + userSearchKey = searchKeyInput.value.trim(); + if (userSearchKey) localStorage.setItem(STORAGE_KEYS.searchKey, userSearchKey); + else localStorage.removeItem(STORAGE_KEYS.searchKey); + + const avail = searchAvailable(); + toolWebSwitch.disabled = !avail; + toolWebRow.classList.toggle("disabled", !avail); + // Losing the key disables a previously-enabled tool. + if (!avail && toolsEnabled.web_search) { + toolsEnabled.web_search = false; + toolWebSwitch.checked = false; + saveTools(); + pushToolsToSession(); + } + toolWebHint.textContent = userSearchKey + ? "Using your key — stored in this browser only." + : "No server key configured. Add your own Serper key to enable web search."; +}); + +// ── Camera ────────────────────────────────────────────────────────────────── + +async function enableCamera() { + if (cameraStream) return; + cameraStream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: "user" }, + audio: false, + }); + camVideo.srcObject = cameraStream; + try { await camVideo.play(); } catch { /* autoplay quirks; muted video is fine */ } + camPip.classList.add("visible"); + camPip.setAttribute("aria-hidden", "false"); + // Lets the footer reflow to the bottom-right (and hide on mobile) while the + // webcam preview occupies the bottom of the stage. + document.body.classList.add("cam-on"); +} + +function disableCamera() { + if (cameraStream) { + for (const t of cameraStream.getTracks()) t.stop(); + cameraStream = null; + } + camVideo.srcObject = null; + camPip.classList.remove("visible"); + camPip.setAttribute("aria-hidden", "true"); + document.body.classList.remove("cam-on"); +} + +/** Auto-start the webcam on arrival (the camera tool is on by default). If the + * user declines the permission, switch the tool off and reflect it in the UI + * rather than nagging. */ +async function autoStartCamera() { + if (!toolsEnabled.camera_snapshot || cameraStream) return; + try { + await enableCamera(); + } catch (err) { + console.warn("[main] camera auto-start declined/failed:", err); + toolsEnabled.camera_snapshot = false; + saveTools(); + syncToolsUi(); + } +} + +/** Track the browser's camera permission so a later re-grant (e.g. the user + * unblocks it from the address bar after a denial) turns the camera back on + * without another toggle, and a revoke turns it off. Best-effort: the + * Permissions API doesn't support "camera" everywhere (e.g. Safari). */ +async function watchCameraPermission() { + try { + const status = await navigator.permissions?.query?.({ name: /** @type {any} */ ("camera") }); + if (!status) return; + status.addEventListener("change", () => { + if (status.state === "granted") { + if (!toolsEnabled.camera_snapshot) { toolsEnabled.camera_snapshot = true; saveTools(); } + void autoStartCamera(); + syncToolsUi(); + } else if (status.state === "denied") { + disableCamera(); + if (toolsEnabled.camera_snapshot) { toolsEnabled.camera_snapshot = false; saveTools(); } + syncToolsUi(); + } + }); + } catch { + // Permissions API unavailable for "camera" — the toggle still re-asks. + } +} + +/** + * Grab the current webcam frame as a downscaled JPEG data URL. The preview is + * mirrored in CSS for a natural self-view, but we draw the raw (un-mirrored) + * video here so the model sees the scene in its true orientation. + * @returns {string | null} + */ +function captureSnapshot() { + if (!cameraStream || !camVideo.videoWidth) return null; + const vw = camVideo.videoWidth; + const vh = camVideo.videoHeight; + const scale = Math.min(1, SNAPSHOT_MAX_EDGE / Math.max(vw, vh)); + const w = Math.max(1, Math.round(vw * scale)); + const h = Math.max(1, Math.round(vh * scale)); + const canvas = document.createElement("canvas"); + canvas.width = w; + canvas.height = h; + const ctx = canvas.getContext("2d"); + if (!ctx) return null; + ctx.drawImage(camVideo, 0, 0, w, h); + return canvas.toDataURL("image/jpeg", SNAPSHOT_QUALITY); +} + +/** Brief shutter flash on the preview so the user sees a snapshot was taken. */ +function flashPreview() { + camPip.classList.remove("flash"); + void camPip.offsetWidth; // reflow so the animation restarts + camPip.classList.add("flash"); +} + +// ── Tool executor ───────────────────────────────────────────────────────── +// Runs the function the model called, returns the result, and asks for a +// response so the model speaks it. Errors come back as the tool output too, so +// the model can recover gracefully instead of the turn stalling. + +/** + * Run the function the model called, return its result to the backend, and ask + * for a follow-up response. We also hand the result back to the caller so it + * can be shown in the conversation once the tool has actually run. + * @param {string} name @param {string} argsJson @param {string} callId + * @returns {Promise<{ output: string, image?: string }>} + */ +async function runTool(name, argsJson, callId) { + if (!client) return { output: "" }; + let args = /** @type {Record} */ ({}); + try { args = JSON.parse(argsJson || "{}"); } catch { /* keep {} */ } + + if (DEBUG) console.debug(`[tool] run name=${name} callId=${JSON.stringify(callId)} args=${argsJson}`); + if (!callId) console.warn("[tool] empty call_id — the backend didn't tag the call, can't return a function_call_output"); + + /** @type {{ output: string, image?: string }} */ + let result = { output: "" }; + try { + if (name === "web_search") { + const query = typeof args.query === "string" ? args.query : ""; + result.output = await execWebSearch(query); + // Return the result and let the bare response.create (below) trigger the + // spoken answer. + client.sendToolOutput(callId, result.output); + } else if (name === "camera_snapshot") { + const dataUrl = captureSnapshot(); + if (dataUrl) { + if (DEBUG) console.debug(`[tool] camera_snapshot captured frame (${dataUrl.length} chars), sending image + output`); + result = { output: "Snapshot captured from the webcam and attached as an image.", image: dataUrl }; + // Return the tool output; the frame itself rides along with the + // response.create below (sent right before it), so the model sees the + // snapshot in the very response it's about to speak. + client.sendToolOutput(callId, result.output); + flashPreview(); + } else { + console.warn("[tool] camera_snapshot: no frame — camera off or not ready"); + result.output = "The camera is not available right now."; + client.sendToolOutput(callId, result.output); + } + } else { + result.output = `Unknown tool: ${name}`; + client.sendToolOutput(callId, result.output); + } + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + result.output = `Tool failed: ${msg}`; + client.sendToolOutput(callId, result.output); + } + if (DEBUG) console.debug(`[tool] requesting model response after ${name}`); + // Camera: the captured frame rides with the response.create (sent just before + // it) so it's in context for the reply. Other tools: a bare create. + client.requestResponse(result.image ? { image: result.image } : undefined); + return result; +} + +/** @param {string} query @returns {Promise} */ +async function execWebSearch(query) { + if (!query) return "No query provided."; + /** @type {Record} */ + const body = { query }; + // Only send a user key when there's no server key (server prefers its own). + if (!serverSearchKey && userSearchKey) body.key = userSearchKey; + + const res = await fetch("api/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!res.ok) { + let detail = String(res.status); + try { const j = await res.json(); if (j.detail) detail = j.detail; } catch {} + throw new Error(`search error (${detail})`); + } + const json = await res.json(); + // Date-stamp the header so the model treats these as fresh realtime facts + // rather than its (older) training knowledge. + const today = new Date().toISOString().slice(0, 10); + /** @type {string[]} */ + const lines = [`Google search result from ${today}:`]; + if (json.answer) lines.push(`Answer: ${json.answer}`); + for (const r of json.results || []) { + lines.push(`- ${r.title}: ${r.snippet} (${r.url})`); + } + return lines.length > 1 ? lines.join("\n") : `${lines[0]}\nNo results found.`; +} + +/** Learn server config (search key + connection target), then refresh the UI. */ +async function fetchConfig() { + try { + const res = await fetch("api/config"); + if (res.ok) { + const json = await res.json(); + serverSearchKey = !!json.search; + lbMode = !!json.lb; + // Lock to LB mode only when the deploy reports a load balancer. + allowDirect = json.allowDirect ?? !lbMode; + // Deploy-pinned direct URL (overrides the LB server-side already). + pinnedUrl = (json.s2sUrl || "").trim(); + // The conversation-time limiter rides on the LB being present. + limiterOn = lbMode; + } + // Non-OK response: leave the fail-open default (allowDirect = true). + } catch { + // Config endpoint unreachable (e.g. static hosting): keep direct entry. + } + if (DEBUG) console.debug(`[ui] config: allowDirect=${allowDirect} lbMode=${lbMode}`); + // Login chip + remaining-budget (no-op / hidden when the limiter is off). + void account.refresh(); + syncToolsUi(); + syncConnectionUi(); +} + +/** + * Resolve where to connect, per the deploy's mode: + * • LB mode -> `{ sessionUrl }`, the client POSTs the same-origin /api/session + * proxy and the server forwards to the LB (its address stays server-side). + * • direct -> `{ directUrl }`, connect straight to the s2s WebSocket. + * Throws a user-facing error if direct mode is on but no URL was entered. + * @returns {{ sessionUrl: string } | { directUrl: string }} + */ +function connectionTarget() { + if (!allowDirect) { + return { sessionUrl: "api/session" }; + } + const directUrl = buildDirectWsUrl(pinnedUrl || settings.directUrl); + if (!directUrl) { + throw new Error("Enter a speech-to-speech server URL in Settings."); + } + return { directUrl }; +} + +/** + * Normalise a user-typed server address into a realtime WebSocket URL. + * Accepts bare hosts (`localhost:8080`), http(s) URLs, or ws(s) URLs, and adds + * the `/v1/realtime` path when none is given. A full connect URL (with path + * and/or query) is preserved as-is. + * @param {string} raw @returns {string} + */ +function buildDirectWsUrl(raw) { + let s = (raw || "").trim(); + if (!s) return ""; + if (!/^wss?:\/\//i.test(s)) { + if (/^https?:\/\//i.test(s)) { + s = s.replace(/^http/i, "ws"); // http→ws, https→wss + } else { + const isLocal = /^(localhost|127\.0\.0\.1|\[::1\])(:|\/|$)/i.test(s); + s = (isLocal ? "ws://" : "wss://") + s; + } + } + try { + const u = new URL(s); + if (u.pathname === "" || u.pathname === "/") u.pathname = "/v1/realtime"; + return u.toString(); + } catch { + return s; + } +} + +/** Create + resume an AudioContext synchronously (must run inside the user + * gesture so iOS lets it start). Returns null if construction fails. */ +function createResumedAudioContext() { + try { + const Ctx = window.AudioContext || /** @type {any} */ (window).webkitAudioContext; + const ctx = new Ctx({ latencyHint: "interactive" }); + if (ctx.state === "suspended") void ctx.resume().catch(() => {}); + return /** @type {AudioContext} */ (ctx); + } catch (err) { + console.warn("[main] AudioContext init failed:", err); + return null; + } +} + +/** Read the editable settings out of the form. The URL field is only honoured + * in free direct mode — in LB mode it's hidden, and when the deploy pins a + * URL it's read-only, so the user's saved URL survives either way. */ +function readSettingsFromForm() { + return { + directUrl: allowDirect && !pinnedUrl ? inputLbUrl.value.trim() : settings.directUrl, + voice: inputVoice.value || DEFAULT_VOICE, + instructions: inputInstructions.value.trim() || DEFAULT_INSTRUCTIONS, + noiseGate: readGateThreshold(), + }; +} + +/** Gate threshold (dBFS) currently shown on the slider, clamped to range. */ +function readGateThreshold() { + const v = Math.round(Number(inputNoiseGate.value)); + if (!Number.isFinite(v)) return GATE_OFF_DB; + return Math.min(GATE_MAX_DB, Math.max(GATE_OFF_DB, v)); +} + +/** Adapt the connection field to the mode learned from /api/config. */ +function syncConnectionUi() { + if (pinnedUrl) { + // Deploy-pinned URL: show it, but locked — the deployment owns it. + connField.hidden = false; + inputLbUrl.value = pinnedUrl; + inputLbUrl.readOnly = true; + connHint.classList.remove("error"); + connHint.textContent = "Speech-to-speech server URL pinned by this deployment."; + } else if (allowDirect) { + // Direct mode: the user sets their own s2s server URL. + connField.hidden = false; + inputLbUrl.value = settings.directUrl; + inputLbUrl.readOnly = false; + inputLbUrl.placeholder = "http://localhost:port"; + connHint.classList.remove("error"); + connHint.textContent = + "URL of your speech-to-speech server, e.g. http://localhost:8080 (the app adds /v1/realtime)."; + } else { + // LB mode: the load balancer URL is deployment-owned — hide it entirely so + // its address is never exposed in Settings. + connField.hidden = true; + } +} + +/** True when the user must supply a server URL before connecting (direct mode + * with nothing set). */ +function missingServerUrl() { + return allowDirect && !pinnedUrl && !buildDirectWsUrl(settings.directUrl); +} + +/** Open Settings and point the user at the empty server-URL field. */ +function promptServerUrl() { + if (settingsModal.open) syncConnectionUi(); + else openSettings(); + connHint.textContent = "Set the speech-to-speech server URL to start."; + connHint.classList.add("error"); + inputLbUrl.focus(); +} + +settingsForm.addEventListener("submit", (event) => { + const submitter = /** @type {HTMLButtonElement | null} */ ((/** @type {SubmitEvent} */ (event)).submitter); + if (submitter?.value !== "save") return; + + settings = readSettingsFromForm(); + saveSettings(settings); + + // Voice + instructions can apply to a live session without reconnecting; a + // changed connection URL only takes effect on the next restart. + if (client && LIVE_STATES.has(currentState)) { + client.updateSession({ voice: settings.voice, instructions: effectiveInstructions() }); + } +}); + +// The noise gate applies live (worklet param), so tune it without a restart: +// update the label/marker, persist, and push straight to the running client. +inputNoiseGate.addEventListener("input", () => { + setGateThreshold(readGateThreshold()); +}); + +restartBtn.addEventListener("click", async () => { + if (currentState === "connecting") return; // a connect is already underway + settings = readSettingsFromForm(); + saveSettings(settings); + if (missingServerUrl()) { promptServerUrl(); return; } // keep settings open + settingsModal.close(); + // Grab the AudioContext NOW, inside the click gesture — teardown() awaits, and + // creating it afterwards would fall outside the gesture (silent on iOS). + const audioContext = createResumedAudioContext(); + try { + if (client) await teardown(); + await doStart(audioContext); + } catch (err) { + await handleStartError(err); + } +}); + +circleBtn.addEventListener("click", async () => { + try { + if (currentState === "idle" || currentState === "error") { + if (missingServerUrl()) { promptServerUrl(); return; } + await doStart(); + } + } catch (err) { + await handleStartError(err); + } +}); + +/** A failed start is either the daily limit (show the modal, return to idle) or + * a real fault (surface it). doStart already closed any orphan AudioContext. + * @param {any} err */ +async function handleStartError(err) { + if (err && err.code === "limit") { + await teardown(); + account.showLimit(err.tier); + return; + } + // The user left the queue (close() aborted the wait): teardown already reset + // the UI to idle, so there's nothing to report. + if (err && err.code === "aborted") return; + // The whole waiting line is full: a warm, reassuring modal rather than an error. + if (err && err.code === "queue-full") { + await teardown(); + account.showBusy(); + return; + } + // Our place lapsed (ticket reaped, or the join window ran out). Recoverable, not + // a fault: land on the retry state with a kind, plain-language reason. + if (err && (err.code === "queue-expired" || err.code === "join-expired")) { + await teardown(); + setState("error"); + setCaption( + err.code === "join-expired" + ? "Your spot expired. Tap to rejoin." + : "That took a while. Tap to rejoin.", + "error", + ); + return; + } + onFatalError(err); +} + +micBtn.addEventListener("click", () => { + if (!micStream || !client) return; + micMuted = !micMuted; + for (const track of micStream.getAudioTracks()) { + track.enabled = !micMuted; + } + client.setMuted(micMuted); + micBtn.classList.toggle("muted", micMuted); + micBtn.setAttribute("aria-label", micMuted ? "Unmute" : "Mute"); + micBtn.title = micMuted ? "Unmute" : "Mute"; +}); + +stopBtn.addEventListener("click", async () => { + await teardown(); +}); + +// "Leave queue": tear down the pending connect (aborts the poll wait) and drop +// our place in line. Same teardown path as stopping a live call. +leaveQueueBtn.addEventListener("click", async () => { + await teardown(); +}); + +// "Join now": accept the held slot. The click is a user gesture, so the client +// re-resumes the AudioContext here (iOS) before dialing. +joinQueueBtn.addEventListener("click", () => { + stopJoinCountdown(); + if (client) client.join(); +}); + +const MIC_CONSTRAINTS = { + audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }, +}; + +/** Prompt for mic permission up front, then immediately release the tracks so no + * recording indicator lingers during a queue wait. Throws a friendly error if the + * user denies. */ +async function primeMicPermission() { + try { + const s = await navigator.mediaDevices.getUserMedia(MIC_CONSTRAINTS); + for (const track of s.getTracks()) track.stop(); + } catch (err) { + throw new Error( + `Microphone access denied${err instanceof Error ? `: ${err.message}` : ""}`, + ); + } +} + +/** Acquire the live capture stream once a slot is granted. Permission was primed + * in the tap gesture, so this is silent. Stored module-side for mute + teardown. */ +async function acquireMicStream() { + micStream = await navigator.mediaDevices.getUserMedia(MIC_CONSTRAINTS); + return micStream; +} + +/** @param {number} position Update the queued caption ("You're #N in line"). */ +function onQueuePosition(position) { + const n = Number(position) || 0; + setCaption(n > 0 ? `You're #${n} in line` : "Finding you a spot…", "muted"); +} + +// ── "Your turn" join countdown ────────────────────────────────────────────── +// While a slot is held for us, show how long is left to accept it. The client's +// join gate expires just before the load balancer reclaims the slot. +let joinCountdownTimer = 0; + +/** @param {number} sec */ +function startJoinCountdown(sec) { + stopJoinCountdown(); + let left = Math.max(0, Math.floor(sec)); + const paint = () => { + joinQueueBtn.textContent = left > 0 ? `Join now (${left}s)` : "Join now"; + }; + paint(); + joinCountdownTimer = window.setInterval(() => { + left -= 1; + if (left <= 0) { + stopJoinCountdown(); + joinQueueBtn.textContent = "Join now"; + return; + } + paint(); + }, 1000); +} + +function stopJoinCountdown() { + if (joinCountdownTimer) { + clearInterval(joinCountdownTimer); + joinCountdownTimer = 0; + } +} + +/** + * Start a conversation. Pass a pre-created AudioContext when the caller already + * made one inside the tap/click gesture (required on iOS); otherwise one is + * created here, which is still inside the gesture for a direct orb tap. + * @param {AudioContext | null} [audioContext] + */ +async function doStart(audioContext = null) { + // Resolve the target before touching mic/audio so a misconfiguration (e.g. + // direct mode with no URL) fails fast with a clear message. + const target = connectionTarget(); + + chat.clear(); + chat.reset(); + setState("connecting"); + setCaption("Asking for mic…", "muted"); + + // Create + resume the AudioContext SYNCHRONOUSLY, still inside the gesture. + // iOS Safari only starts an AudioContext from a user gesture; if we waited + // until after the getUserMedia / session-creation awaits below, it would stay + // suspended and the whole pipeline would be silent. + if (!audioContext) audioContext = createResumedAudioContext(); + + // Prime the mic permission now (get the prompt out of the way up front), then + // release it. The real capture stream is acquired only once a slot is granted + // (see acquireMicStream), so the mic 'in use' indicator never lights while we + // sit in the queue. Permission persists, so the later acquire is silent. + try { + await primeMicPermission(); + } catch (err) { + if (audioContext) void audioContext.close().catch(() => {}); + throw err; + } + + // The webcam is started on arrival (autoStartCamera), so nothing to do here; + // a still-pending grant just means the snapshot tool isn't ready yet. + + const c = new S2sWsRealtimeClient({ + ...target, + voice: settings.voice, + instructions: effectiveInstructions(), + acquireMic: acquireMicStream, + tools: activeToolDefs(), + noiseGate: gateParams(settings.noiseGate), + ...(audioContext ? { audioContext } : {}), + }); + client = c; + + c.addEventListener("queue", (e) => { + const { position, queueId } = /** @type {CustomEvent<{ position: number; queueId: string }>} */ (e).detail; + if (queueId) queuedTicketId = queueId; + onQueuePosition(position); + }); + + c.addEventListener("ready-to-join", (e) => { + const { info, expiresSec } = /** @type {CustomEvent<{ info: import("./ws/s2s-ws-client.js").WsSessionInfo; expiresSec: number }>} */ (e).detail; + // A slot is held for us. We're out of the queue now, so drop the ticket ref. + // Track the granted session id already so that leaving (or letting the timer + // lapse) refunds the budget the server reserved at claim, even before we dial. + queuedTicketId = ""; + if (info?.sessionId) { + trackedSessionId = info.sessionId; + trackedTier = info.tier || "anon"; + } + startJoinCountdown(expiresSec); + }); + + c.addEventListener("status", (e) => { + const detail = /** @type {CustomEvent<{ status: string }>} */ (e).detail; + onClientStatus(detail.status); + }); + c.addEventListener("transcript", (e) => { + const d = /** @type {CustomEvent<{ role: "user" | "assistant"; text: string; partial: boolean; itemId?: string; responseId?: string }>} */ (e).detail; + chat.onTranscript(d); + }); + + c.addEventListener("response-finished", (e) => { + const detail = /** @type {CustomEvent<{ responseId: string; status: string; audible?: boolean; transcript?: string }>} */ (e).detail; + chat.onResponseFinished(detail); + }); + + c.addEventListener("toolcall", (e) => { + const { name, arguments: args, callId } = /** @type {CustomEvent<{ name: string; arguments: string; callId: string }>} */ (e).detail; + chat.onToolCall(name); + // Execute the tool, then push it to the conversation once the result is in, + // so the toggle shows both the call input and its output together. + void runTool(name, args, callId).then(({ output, image }) => { + chat.onToolResult(name, args, output, image); + }); + }); + c.addEventListener("error", (e) => { + const detail = /** @type {CustomEvent<{ error: unknown }>} */ (e).detail; + onFatalError(detail.error); + }); + c.addEventListener("server-error", (e) => { + // Non-fatal: the backend reported an error mid-session. Log it, keep the + // socket and the conversation alive (the model can recover on its own). + const detail = /** @type {CustomEvent<{ error: unknown }>} */ (e).detail; + const msg = detail.error instanceof Error ? detail.error.message : String(detail.error); + console.warn("[main] server error (non-fatal):", msg); + }); + c.addEventListener("session", (e) => { + const info = /** @type {CustomEvent<{ info: import("./ws/s2s-ws-client.js").WsSessionInfo }>} */ (e).detail.info; + console.log("[ws] session created:", info.sessionId); + // A slot was granted — we're out of the queue; drop the ticket reference so + // teardown doesn't try to leave a line we already left. + queuedTicketId = ""; + // A metered tier (anon / free): heartbeat so the server can extend the + // reservation and tell us when the daily budget runs out. PRO isn't limited. + if (info.limited && info.sessionId) { + trackedSessionId = info.sessionId; + trackedTier = info.tier || "anon"; + startHeartbeat(info.heartbeatSec || 5); + } + }); + c.addEventListener("input-level", (e) => { + const { rms } = /** @type {CustomEvent<{ rms: number }>} */ (e).detail; + paintInputLevel(rms); + }); + + try { + await c.connect(); + } catch (err) { + // The grant can be refused (402 → limit) or the dial can fail. In LB mode + // the AudioContext hasn't been adopted by the client yet (the session POST + // runs first), so close the one we created here to avoid leaking it. + if (audioContext) void audioContext.close().catch(() => {}); + throw err; + } +} + +// ── Conversation-time heartbeat ───────────────────────────────────────────── + +/** Ping the server every `sec` seconds so it can meter the live session; when + * it reports the daily budget is spent, cut the call and show the limit modal. + * @param {number} sec */ +function startHeartbeat(sec) { + stopHeartbeat(); + heartbeatTimer = window.setInterval(async () => { + if (!trackedSessionId) return; + try { + const res = await fetch("api/session/heartbeat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId: trackedSessionId }), + keepalive: true, + }); + const json = await res.json().catch(() => ({})); + if (json.expired) await onLimitReached(); + } catch (err) { + // A transient network blip shouldn't kill the call; the next tick retries. + if (DEBUG) console.debug("[ui] heartbeat failed:", err); + } + }, Math.max(1, sec) * 1000); +} + +function stopHeartbeat() { + if (heartbeatTimer) { + clearInterval(heartbeatTimer); + heartbeatTimer = 0; + } +} + +/** The server cut the live session: tear down and explain why. */ +async function onLimitReached() { + const tier = trackedTier; + stopHeartbeat(); + await teardown(); + account.showLimit(tier); +} + +/** Tell the server a session ended so it reconciles + refunds the unused chunk. + * Uses sendBeacon so it still fires when the tab is closing. */ +function endTrackedSession() { + if (!trackedSessionId) return; + const body = JSON.stringify({ sessionId: trackedSessionId }); + try { + const blob = new Blob([body], { type: "application/json" }); + if (!navigator.sendBeacon("api/session/end", blob)) { + void fetch("api/session/end", { + method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true, + }).catch(() => {}); + } + } catch { + // Best-effort; the server sweep reaps the session anyway. + } + trackedSessionId = ""; + trackedTier = ""; +} + +/** Leave the waiting queue so the LB frees our place. sendBeacon so it still + * fires on tab close; the LB also reaps the ticket on TTL as a backstop. */ +function endQueueTicket() { + if (!queuedTicketId) return; + const body = JSON.stringify({ queueId: queuedTicketId }); + try { + const blob = new Blob([body], { type: "application/json" }); + if (!navigator.sendBeacon("api/queue/end", blob)) { + void fetch("api/queue/end", { + method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true, + }).catch(() => {}); + } + } catch { + // Best-effort; the LB reaps the ticket on TTL anyway. + } + queuedTicketId = ""; +} + +/** @param {string} status */ +function onClientStatus(status) { + switch (status) { + case "creating-session": + case "connecting": + setState("connecting"); + break; + case "queued": + setState("queued"); + break; + case "your-turn": + setState("your-turn"); + break; + case "connected": + setState("listening"); + break; + case "user-speaking": + setState("user-speaking"); + break; + case "processing": + setState("processing"); + break; + case "ai-speaking": + setState("ai-speaking"); + break; + case "closed": + // teardown() will move us to idle + break; + case "error": + setState("error"); + break; + } +} + +async function teardown() { + stopHeartbeat(); + stopJoinCountdown(); + endTrackedSession(); + endQueueTicket(); + chat.reset({ dismiss: true }); + if (client) { + try { + await client.close(); + } catch (err) { + console.warn("[main] error closing client:", err); + } + client = null; + } + if (micStream) { + for (const track of micStream.getTracks()) track.stop(); + micStream = null; + } + // The webcam is independent of the call lifecycle (it runs while the user is + // on the page), so we leave it on here — only the camera toggle stops it. + micMuted = false; + micBtn.classList.remove("muted"); + setState("idle"); + // Refresh the chip's remaining-today after the budget moved. + if (limiterOn) void account.refresh(); +} + +/** @param {unknown} err */ +function onFatalError(err) { + console.error("[main] fatal:", err); + setState("error"); + const message = err instanceof Error ? err.message : String(err); + setCaption(truncateError(message), "error"); + void teardown().catch(() => { + setState("error"); + setCaption(truncateError(message), "error"); + }); +} + +setState("idle"); +chat.renderEmptyState(); +initGateArc(); +void fetchConfig(); +// Start the webcam as soon as the user lands (camera tool defaults on), and +// react to later permission changes (re-grant after a denial re-enables it). +void autoStartCamera(); +void watchCameraPermission(); + +// Reconcile a live session if the tab is closed/hidden mid-call (no teardown). +window.addEventListener("pagehide", () => { endTrackedSession(); endQueueTicket(); }); + +requestAnimationFrame(() => { + document.body.classList.remove("booting"); +}); diff --git a/backend/vendor/s2s-cloud/demo/mictest.html b/backend/vendor/s2s-cloud/demo/mictest.html new file mode 100644 index 0000000..3b5a4a0 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/mictest.html @@ -0,0 +1,75 @@ + + + + + +麦克风诊断 + + + +

🎤 麦克风诊断页(只请求麦克风)

+

在 你刚才打开主页面的那个浏览器窗口 打开本页,点下面按钮。

+ +
等待操作…
+
+ + + + diff --git a/backend/vendor/s2s-cloud/demo/requirements.txt b/backend/vendor/s2s-cloud/demo/requirements.txt new file mode 100644 index 0000000..fc64b9c --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/requirements.txt @@ -0,0 +1,5 @@ +fastapi==0.115.6 +uvicorn[standard]==0.34.0 +httpx==0.28.1 +# [oauth] extra pulls authlib + itsdangerous for attach_huggingface_oauth. +huggingface_hub[oauth]>=0.30,<1.0 diff --git a/backend/vendor/s2s-cloud/demo/server.py b/backend/vendor/s2s-cloud/demo/server.py new file mode 100644 index 0000000..546520f --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/server.py @@ -0,0 +1,484 @@ +""" +Tiny server for the speech-to-speech demo. + +The demo used to ship as a `sdk: static` Space, but the web-search tool needs a +search key the browser must NOT see. A static Space has no runtime process, so it +can't hold a secret the front-end uses. This server fixes that: it serves the +unchanged front-end AND exposes a same-origin `/api/search` proxy that holds the +Serper key server-side (see docs/adr/0001). + +Everything lives in one container; the speech-to-speech backend stays a separate, +load-balanced service the browser talks to over WebSocket as before. The load +balancer's address is a secret too (like the Serper key): the browser never sees +it. `/api/session` proxies the session handshake server-side so only the +per-session compute URL the LB hands back (which the browser must dial) is exposed. + +On the deployed Space the server also meters conversation time by HF login tier +(anonymous / signed-in / PRO) — see `limiter.py` and `auth.py`. That whole feature +is off unless BOTH `LOAD_BALANCER_URL` and `SPACE_ID` are set, so it runs only on +the live Space, never locally (even with the LB exported for testing). + +`SPEECH_TO_SPEECH_URL` overrides everything: when set, the LB logic above is +disabled entirely (no session proxy, no queue, no metering, no sign-in) and the +browser connects directly to that URL, shown read-only in Settings. + +Endpoints: + GET /api/config -> { search, lb, allowDirect, s2sUrl, auth } + GET /api/me -> login + tier + remaining budget (LB mode only) + POST /api/search -> { results, answer } Google via Serper.dev + POST /api/session -> proxies /session: a grant, or a queue ticket + GET /api/queue/{id} -> proxies /queue/{id}: position, or a grant on claim + DELETE /api/queue/{id} -> leave the queue (explicit "Leave queue" button) + POST /api/queue/end -> leave the queue (sendBeacon on teardown) + POST /api/session/heartbeat-> extend the reservation; { expired } + POST /api/session/end -> reconcile + refund (sendBeacon on teardown) + /* -> static files (index.html, main.js, ...) + +When every compute slot is busy the load balancer hands back a queue ticket +instead of a grant; the browser polls /api/queue/{id} until it reaches the front +and a slot frees. Waiting reserves nothing — the daily budget is only reserved at +the moment a slot is actually claimed (a grant), never while queued. +""" + +import asyncio +import logging +import os + +import httpx +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +from . import auth +from . import limiter + +logger = logging.getLogger("s2s.search") + +SERPER_KEY = os.environ.get("SERPER_API_KEY", "").strip() +# Speech-to-speech load balancer URL. When set, the browser POSTs /api/session +# (which proxies /session here, server-side) and connects to the URL the LB +# returns (the original flow). The LB address itself is never sent to the browser. +# When empty, the user may instead set a direct s2s server URL in Settings and the +# browser connects to it straight (no load balancer). +LOAD_BALANCER_URL = os.environ.get("LOAD_BALANCER_URL", "").strip() +# Direct s2s server URL pinned by the deploy. Takes priority over the load +# balancer: when set, ALL LB logic is disabled (no /api/session proxy, no queue, +# no limiter, no sign-in) and the browser connects to this URL directly. Unlike +# the LB address it is NOT a secret — /api/config sends it to the client, which +# shows it read-only in Settings. +SPEECH_TO_SPEECH_URL = os.environ.get("SPEECH_TO_SPEECH_URL", "").strip() +if SPEECH_TO_SPEECH_URL: + LOAD_BALANCER_URL = "" +# HF injects SPACE_ID ("owner/space") into every Space runtime; it's absent +# locally and on a plain `docker run`. We meter conversation time ONLY on the +# deployed Space — i.e. when BOTH the LB is configured AND we're on a Space. +# Off-Space (local dev, even with the LB exported) the app still proxies the LB, +# but nothing is metered: no budget, no reservations, no sign-in gating. +SPACE_ID = os.environ.get("SPACE_ID", "").strip() +LIMITER_ENABLED = bool(LOAD_BALANCER_URL) and bool(SPACE_ID) +SERPER_URL = "https://google.serper.dev/search" +# Cap results so the tool output stays small enough to feed back to the model. +MAX_RESULTS = 5 +HERE = os.path.dirname(os.path.abspath(__file__)) +UPLOAD_DIR = os.path.join(HERE, "uploads") +os.makedirs(UPLOAD_DIR, exist_ok=True) + +app = FastAPI(title="s2s-demo") + +# Wire HF OAuth before the app serves (no-op unless the OAuth env is present). +# Sign-in only matters when we're metering (prod Space), so gate it on that. +AUTH_ENABLED = LIMITER_ENABLED and auth.attach(app) + + +@app.on_event("startup") +async def _startup(): + """Stand up the usage DB and a periodic sweeper — metered (prod Space) only.""" + if not LIMITER_ENABLED: + return + limiter.init() + asyncio.create_task(_sweeper()) + + +async def _sweeper(): + while True: + await asyncio.sleep(limiter.REAP_AFTER_SEC) + try: + await asyncio.to_thread(limiter.sweep) + except Exception as exc: # pragma: no cover - defensive + logger.warning("usage sweep failed: %r", exc) + + +class SearchRequest(BaseModel): + query: str + # Optional user-supplied key (fallback when the deploy has no server key). + # Used for this request only; never stored. + key: str | None = None + + +@app.get("/api/config") +def config(): + """Client bootstrap: whether web search is available, whether the deploy runs + behind a load balancer (so the browser uses the /api/session proxy + limiter), + whether HF sign-in is available, and whether the user may instead set a direct + s2s server URL. The LB address itself is intentionally NOT included.""" + return { + "search": bool(SERPER_KEY), + "lb": bool(LOAD_BALANCER_URL), + "allowDirect": not LOAD_BALANCER_URL, + # Deploy-pinned direct s2s URL (empty when unset). Not a secret: the + # browser dials it itself, and Settings shows it locked. + "s2sUrl": SPEECH_TO_SPEECH_URL, + "auth": AUTH_ENABLED, + } + + +@app.get("/api/me") +async def me(request: Request): + """Login state, tier, and remaining daily budget. Only meaningful in LB mode; + sets the anonymous tracking cookie when first seen.""" + if not LIMITER_ENABLED: + return {"enabled": False} + view = auth.user_view(request) + tier, keys, set_cookie = auth.resolve_identity(request) + unlimited = limiter.budget_for(tier) is None + rem = None if unlimited else await asyncio.to_thread(limiter.remaining, keys, tier) + out = { + "enabled": True, + "auth": AUTH_ENABLED, + **view, + "remainingSec": rem, + "limitSec": limiter.budget_for(tier), + "loginUrl": auth.OAUTH_LOGIN_PATH if AUTH_ENABLED else None, + "logoutUrl": auth.OAUTH_LOGOUT_PATH if AUTH_ENABLED else None, + } + resp = JSONResponse(out) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + +@app.post("/api/search") +async def search(req: SearchRequest): + """Proxy a Google search via Serper.dev. The key stays on the server unless + the user brought their own (then theirs is used for this request only).""" + query = (req.query or "").strip() + if not query: + raise HTTPException(status_code=400, detail="Empty query.") + + key = (req.key or "").strip() or SERPER_KEY + if not key: + # No server key and the user didn't supply one — search is unavailable. + raise HTTPException(status_code=503, detail="Search is not configured.") + + headers = {"X-API-KEY": key, "Content-Type": "application/json"} + payload = {"q": query, "num": MAX_RESULTS} + try: + async with httpx.AsyncClient(timeout=12.0) as http: + resp = await http.post(SERPER_URL, headers=headers, json=payload) + except httpx.RequestError as exc: + logger.warning("Serper unreachable: %r", exc) + raise HTTPException(status_code=502, detail="Search provider unreachable.") + + if resp.status_code != 200: + # Serper's error body carries the real reason (e.g. "Not enough + # credits") and contains no key, so it's safe to log and relay. + body = resp.text[:300] + logger.warning("Serper error %s: %s", resp.status_code, body) + msg = None + try: + msg = resp.json().get("message") + except Exception: + pass + detail = f"Search provider error ({resp.status_code})" + if msg: + detail += f": {msg}" + raise HTTPException(status_code=502, detail=detail) + + data = resp.json() + results = [] + for item in (data.get("organic") or [])[:MAX_RESULTS]: + results.append( + { + "title": item.get("title", ""), + "snippet": item.get("snippet", ""), + "url": item.get("link", ""), + } + ) + + # A direct answer when Google has one — saves the model a hop. + box = data.get("answerBox") or {} + answer = box.get("answer") or box.get("snippet") or None + if not answer: + kg = data.get("knowledgeGraph") or {} + answer = kg.get("description") or None + + return JSONResponse({"query": query, "answer": answer, "results": results}) + + +@app.post("/api/session") +async def session(request: Request): + """Proxy the session handshake to the load balancer, keeping its URL secret, + and meter conversation time by tier. + + The browser POSTs here (same-origin); we resolve the caller's tier, refuse if + today's budget is already spent (402), otherwise POST /session + and relay the JSON back. The LB body carries a per-session `connect_url` + (compute host + short-lived token) the browser must dial directly — that one + URL is unavoidably exposed, but the stable load-balancer address is not. On a + successful grant we reserve the first time chunk against the day's budget.""" + if not LOAD_BALANCER_URL: + # No LB configured — this deploy is direct-mode only; the browser should + # never call this. 404 so it's indistinguishable from a missing route. + raise HTTPException(status_code=404, detail="Not found.") + + tier, keys, set_cookie = auth.resolve_identity(request) + # Metering runs only on the deployed Space; off-Space the LB still proxies but + # nothing is tracked. Within metering, unlimited tiers (pro, org) aren't either. + tracked = LIMITER_ENABLED and limiter.budget_for(tier) is not None + + # Refuse before troubling the LB if the day's budget is already gone. Done + # here (at enqueue) so we never put a user who can't talk into the queue. + if tracked: + rem = await asyncio.to_thread(limiter.remaining, keys, tier) + if rem is not None and rem <= 0: + resp = JSONResponse( + {"tier": tier, "reason": "limit", "remainingSec": 0}, status_code=402 + ) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + url = f"{LOAD_BALANCER_URL.rstrip('/')}/session" + try: + async with httpx.AsyncClient(timeout=15.0) as http: + lb = await http.post(url, headers={"Content-Type": "application/json"}, content="{}") + except httpx.RequestError as exc: + logger.warning("Load balancer unreachable: %r", exc) + raise HTTPException(status_code=502, detail="Speech service unreachable.") + + # The queue is full: the LB replies 503 {state:"at_capacity"}. Relay it as-is + # so the client shows a soft "try again shortly", not a hard error. + if lb.status_code == 503: + body = _safe_json(lb) + if body.get("state") == "at_capacity": + resp = JSONResponse({"state": "at_capacity"}, status_code=503) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + if lb.status_code != 200: + # The LB's error body may name the reason (e.g. capacity); it carries no + # secret, so relay a trimmed copy. + logger.warning("Session handshake failed %s: %s", lb.status_code, lb.text[:300]) + raise HTTPException(status_code=502, detail=f"Session handshake failed ({lb.status_code}).") + + data = lb.json() + + # Busy pool: the LB queued us. Relay the ticket untouched — crucially with NO + # reservation, so waiting in line never costs the day's budget. + if data.get("state") == "queued": + data["tier"] = tier + resp = JSONResponse(data) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + # A slot was free: reserve the first chunk now and return the grant. + return await _finalize_grant(data, keys, tier, tracked, set_cookie) + + +@app.get("/api/queue/{queue_id}") +async def queue_status(queue_id: str, request: Request): + """Poll a waiting ticket: relay the position, or — when the head of the line + claims a freed slot — reserve the budget now and return the grant. Re-checks the + daily budget at claim, since a multi-minute wait could have spent it elsewhere.""" + if not LOAD_BALANCER_URL: + raise HTTPException(status_code=404, detail="Not found.") + + tier, keys, set_cookie = auth.resolve_identity(request) + tracked = LIMITER_ENABLED and limiter.budget_for(tier) is not None + + url = f"{LOAD_BALANCER_URL.rstrip('/')}/queue/{queue_id}" + try: + async with httpx.AsyncClient(timeout=15.0) as http: + lb = await http.get(url) + except httpx.RequestError as exc: + logger.warning("Load balancer unreachable: %r", exc) + raise HTTPException(status_code=502, detail="Speech service unreachable.") + + if lb.status_code == 404: + # Ticket unknown/expired (reaped after we stopped polling). Tell the client + # to start over rather than spin. + resp = JSONResponse({"state": "expired"}, status_code=404) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + if lb.status_code != 200: + logger.warning("Queue poll failed %s: %s", lb.status_code, lb.text[:300]) + raise HTTPException(status_code=502, detail=f"Queue poll failed ({lb.status_code}).") + + data = lb.json() + + if data.get("state") == "queued": + data["tier"] = tier + resp = JSONResponse(data) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + # Claimed a slot. Re-check the budget: it may have been spent in another tab + # during the wait. If so, refuse — the just-claimed slot is now a pending + # session on the LB and its pending-timeout reaper reclaims it shortly. + if tracked: + rem = await asyncio.to_thread(limiter.remaining, keys, tier) + if rem is not None and rem <= 0: + resp = JSONResponse( + {"tier": tier, "reason": "limit", "remainingSec": 0}, status_code=402 + ) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + return await _finalize_grant(data, keys, tier, tracked, set_cookie) + + +@app.delete("/api/queue/{queue_id}") +async def queue_leave(queue_id: str): + """Leave the queue from the explicit 'Leave queue' button (a real fetch).""" + if not LOAD_BALANCER_URL: + raise HTTPException(status_code=404, detail="Not found.") + await _lb_leave(queue_id) + return {"ok": True} + + +@app.post("/api/queue/end") +async def queue_end(request: Request): + """Leave the queue on teardown/tab-close (navigator.sendBeacon, which can only + POST). Body: { queueId }. Best-effort; the LB reaps the ticket on TTL anyway.""" + if not LOAD_BALANCER_URL: + raise HTTPException(status_code=404, detail="Not found.") + qid = await _queue_id(request) + if qid: + await _lb_leave(qid) + return {"ok": True} + + +async def _finalize_grant(data, keys, tier, tracked, set_cookie): + """Shared grant tail (fast path or queue claim): reserve the first chunk, attach + the metering fields the client needs, and set the anon cookie.""" + remaining = None + if tracked and data.get("session_id"): + await asyncio.to_thread(limiter.begin, data["session_id"], keys, tier) + remaining = await asyncio.to_thread(limiter.remaining, keys, tier) + + data.update({ + "tier": tier, + "limited": tracked, + "remainingSec": remaining, + "heartbeatSec": limiter.HEARTBEAT_SEC, + }) + resp = JSONResponse(data) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + +async def _lb_leave(queue_id: str) -> None: + """Best-effort: tell the LB to drop a waiting ticket.""" + url = f"{LOAD_BALANCER_URL.rstrip('/')}/queue/{queue_id}" + try: + async with httpx.AsyncClient(timeout=5.0) as http: + await http.delete(url) + except httpx.RequestError as exc: + logger.warning("Queue leave failed: %r", exc) + + +def _safe_json(response) -> dict: + try: + body = response.json() + except Exception: + return {} + return body if isinstance(body, dict) else {} + + +async def _queue_id(request: Request) -> str: + """Pull `queueId` from a JSON body, tolerating sendBeacon's blob posts.""" + try: + data = await request.json() + except Exception: + return "" + return (data or {}).get("queueId", "") if isinstance(data, dict) else "" + + +async def _session_id(request: Request) -> str: + """Pull `sessionId` from a JSON body, tolerating sendBeacon's blob posts.""" + try: + data = await request.json() + except Exception: + return "" + return (data or {}).get("sessionId", "") if isinstance(data, dict) else "" + + +@app.post("/api/session/heartbeat") +async def session_heartbeat(request: Request): + """Extend the live reservation one chunk at a time. `expired` once the day's + budget is spent — the client then tears down.""" + if not LIMITER_ENABLED: + raise HTTPException(status_code=404, detail="Not found.") + sid = await _session_id(request) + alive = bool(sid) and await asyncio.to_thread(limiter.heartbeat, sid) + return {"expired": not alive} + + +@app.post("/api/session/end") +async def session_end(request: Request): + """Clean teardown: reconcile to real elapsed time and refund the unused + chunk. Sent via navigator.sendBeacon, so it must succeed without a response.""" + if not LIMITER_ENABLED: + raise HTTPException(status_code=404, detail="Not found.") + sid = await _session_id(request) + if sid: + await asyncio.to_thread(limiter.end, sid) + return {"ok": True} + + +@app.post("/api/upload_voice") +async def upload_voice(request: Request): + """Upload a custom voice sample for voice cloning. + Accepts a multipart form with a 'file' field containing a WAV/MP3/OGG audio file. + Returns the absolute path to the saved file, which can be used as the voice setting. + """ + form = await request.form() + if "file" not in form: + raise HTTPException(status_code=400, detail="No file provided.") + file = form["file"] + if not file.filename: + raise HTTPException(status_code=400, detail="Empty filename.") + # Sanitize filename + safe_name = os.path.basename(file.filename) + filepath = os.path.join(UPLOAD_DIR, safe_name) + content = await file.read() + with open(filepath, "wb") as f: + f.write(content) + abs_path = os.path.abspath(filepath) + logger.info("Saved uploaded voice sample to %s", abs_path) + return {"path": abs_path, "filename": safe_name} + + +@app.get("/api/voices") +async def list_voices(): + """List available voice uploads and built-in speakers.""" + speakers = ["Aiden", "Ryan", "Dylan", "Eric", "Ono_Anna", "Serena", "Sohee", "Uncle_Fu", "Vivian"] + uploads = [] + if os.path.isdir(UPLOAD_DIR): + for f in sorted(os.listdir(UPLOAD_DIR)): + if f.lower().endswith((".wav", ".mp3", ".ogg", ".flac", ".m4a")): + uploads.append({"filename": f, "path": os.path.abspath(os.path.join(UPLOAD_DIR, f))}) + return {"speakers": speakers, "uploads": uploads} + + +# Static front-end. Registered last so the /api routes win. `html=True` serves +# index.html at "/". The repo is public anyway, so serving the dir is fine. +app.mount("/", StaticFiles(directory=HERE, html=True), name="static") diff --git a/backend/vendor/s2s-cloud/demo/server.py.orig b/backend/vendor/s2s-cloud/demo/server.py.orig new file mode 100644 index 0000000..ba6b45e --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/server.py.orig @@ -0,0 +1,447 @@ +""" +Tiny server for the speech-to-speech demo. + +The demo used to ship as a `sdk: static` Space, but the web-search tool needs a +search key the browser must NOT see. A static Space has no runtime process, so it +can't hold a secret the front-end uses. This server fixes that: it serves the +unchanged front-end AND exposes a same-origin `/api/search` proxy that holds the +Serper key server-side (see docs/adr/0001). + +Everything lives in one container; the speech-to-speech backend stays a separate, +load-balanced service the browser talks to over WebSocket as before. The load +balancer's address is a secret too (like the Serper key): the browser never sees +it. `/api/session` proxies the session handshake server-side so only the +per-session compute URL the LB hands back (which the browser must dial) is exposed. + +On the deployed Space the server also meters conversation time by HF login tier +(anonymous / signed-in / PRO) — see `limiter.py` and `auth.py`. That whole feature +is off unless BOTH `LOAD_BALANCER_URL` and `SPACE_ID` are set, so it runs only on +the live Space, never locally (even with the LB exported for testing). + +`SPEECH_TO_SPEECH_URL` overrides everything: when set, the LB logic above is +disabled entirely (no session proxy, no queue, no metering, no sign-in) and the +browser connects directly to that URL, shown read-only in Settings. + +Endpoints: + GET /api/config -> { search, lb, allowDirect, s2sUrl, auth } + GET /api/me -> login + tier + remaining budget (LB mode only) + POST /api/search -> { results, answer } Google via Serper.dev + POST /api/session -> proxies /session: a grant, or a queue ticket + GET /api/queue/{id} -> proxies /queue/{id}: position, or a grant on claim + DELETE /api/queue/{id} -> leave the queue (explicit "Leave queue" button) + POST /api/queue/end -> leave the queue (sendBeacon on teardown) + POST /api/session/heartbeat-> extend the reservation; { expired } + POST /api/session/end -> reconcile + refund (sendBeacon on teardown) + /* -> static files (index.html, main.js, ...) + +When every compute slot is busy the load balancer hands back a queue ticket +instead of a grant; the browser polls /api/queue/{id} until it reaches the front +and a slot frees. Waiting reserves nothing — the daily budget is only reserved at +the moment a slot is actually claimed (a grant), never while queued. +""" + +import asyncio +import logging +import os + +import httpx +from fastapi import FastAPI, HTTPException, Request +from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +import auth +import limiter + +logger = logging.getLogger("s2s.search") + +SERPER_KEY = os.environ.get("SERPER_API_KEY", "").strip() +# Speech-to-speech load balancer URL. When set, the browser POSTs /api/session +# (which proxies /session here, server-side) and connects to the URL the LB +# returns (the original flow). The LB address itself is never sent to the browser. +# When empty, the user may instead set a direct s2s server URL in Settings and the +# browser connects to it straight (no load balancer). +LOAD_BALANCER_URL = os.environ.get("LOAD_BALANCER_URL", "").strip() +# Direct s2s server URL pinned by the deploy. Takes priority over the load +# balancer: when set, ALL LB logic is disabled (no /api/session proxy, no queue, +# no limiter, no sign-in) and the browser connects to this URL directly. Unlike +# the LB address it is NOT a secret — /api/config sends it to the client, which +# shows it read-only in Settings. +SPEECH_TO_SPEECH_URL = os.environ.get("SPEECH_TO_SPEECH_URL", "").strip() +if SPEECH_TO_SPEECH_URL: + LOAD_BALANCER_URL = "" +# HF injects SPACE_ID ("owner/space") into every Space runtime; it's absent +# locally and on a plain `docker run`. We meter conversation time ONLY on the +# deployed Space — i.e. when BOTH the LB is configured AND we're on a Space. +# Off-Space (local dev, even with the LB exported) the app still proxies the LB, +# but nothing is metered: no budget, no reservations, no sign-in gating. +SPACE_ID = os.environ.get("SPACE_ID", "").strip() +LIMITER_ENABLED = bool(LOAD_BALANCER_URL) and bool(SPACE_ID) +SERPER_URL = "https://google.serper.dev/search" +# Cap results so the tool output stays small enough to feed back to the model. +MAX_RESULTS = 5 +HERE = os.path.dirname(os.path.abspath(__file__)) + +app = FastAPI(title="s2s-demo") + +# Wire HF OAuth before the app serves (no-op unless the OAuth env is present). +# Sign-in only matters when we're metering (prod Space), so gate it on that. +AUTH_ENABLED = LIMITER_ENABLED and auth.attach(app) + + +@app.on_event("startup") +async def _startup(): + """Stand up the usage DB and a periodic sweeper — metered (prod Space) only.""" + if not LIMITER_ENABLED: + return + limiter.init() + asyncio.create_task(_sweeper()) + + +async def _sweeper(): + while True: + await asyncio.sleep(limiter.REAP_AFTER_SEC) + try: + await asyncio.to_thread(limiter.sweep) + except Exception as exc: # pragma: no cover - defensive + logger.warning("usage sweep failed: %r", exc) + + +class SearchRequest(BaseModel): + query: str + # Optional user-supplied key (fallback when the deploy has no server key). + # Used for this request only; never stored. + key: str | None = None + + +@app.get("/api/config") +def config(): + """Client bootstrap: whether web search is available, whether the deploy runs + behind a load balancer (so the browser uses the /api/session proxy + limiter), + whether HF sign-in is available, and whether the user may instead set a direct + s2s server URL. The LB address itself is intentionally NOT included.""" + return { + "search": bool(SERPER_KEY), + "lb": bool(LOAD_BALANCER_URL), + "allowDirect": not LOAD_BALANCER_URL, + # Deploy-pinned direct s2s URL (empty when unset). Not a secret: the + # browser dials it itself, and Settings shows it locked. + "s2sUrl": SPEECH_TO_SPEECH_URL, + "auth": AUTH_ENABLED, + } + + +@app.get("/api/me") +async def me(request: Request): + """Login state, tier, and remaining daily budget. Only meaningful in LB mode; + sets the anonymous tracking cookie when first seen.""" + if not LIMITER_ENABLED: + return {"enabled": False} + view = auth.user_view(request) + tier, keys, set_cookie = auth.resolve_identity(request) + unlimited = limiter.budget_for(tier) is None + rem = None if unlimited else await asyncio.to_thread(limiter.remaining, keys, tier) + out = { + "enabled": True, + "auth": AUTH_ENABLED, + **view, + "remainingSec": rem, + "limitSec": limiter.budget_for(tier), + "loginUrl": auth.OAUTH_LOGIN_PATH if AUTH_ENABLED else None, + "logoutUrl": auth.OAUTH_LOGOUT_PATH if AUTH_ENABLED else None, + } + resp = JSONResponse(out) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + +@app.post("/api/search") +async def search(req: SearchRequest): + """Proxy a Google search via Serper.dev. The key stays on the server unless + the user brought their own (then theirs is used for this request only).""" + query = (req.query or "").strip() + if not query: + raise HTTPException(status_code=400, detail="Empty query.") + + key = (req.key or "").strip() or SERPER_KEY + if not key: + # No server key and the user didn't supply one — search is unavailable. + raise HTTPException(status_code=503, detail="Search is not configured.") + + headers = {"X-API-KEY": key, "Content-Type": "application/json"} + payload = {"q": query, "num": MAX_RESULTS} + try: + async with httpx.AsyncClient(timeout=12.0) as http: + resp = await http.post(SERPER_URL, headers=headers, json=payload) + except httpx.RequestError as exc: + logger.warning("Serper unreachable: %r", exc) + raise HTTPException(status_code=502, detail="Search provider unreachable.") + + if resp.status_code != 200: + # Serper's error body carries the real reason (e.g. "Not enough + # credits") and contains no key, so it's safe to log and relay. + body = resp.text[:300] + logger.warning("Serper error %s: %s", resp.status_code, body) + msg = None + try: + msg = resp.json().get("message") + except Exception: + pass + detail = f"Search provider error ({resp.status_code})" + if msg: + detail += f": {msg}" + raise HTTPException(status_code=502, detail=detail) + + data = resp.json() + results = [] + for item in (data.get("organic") or [])[:MAX_RESULTS]: + results.append( + { + "title": item.get("title", ""), + "snippet": item.get("snippet", ""), + "url": item.get("link", ""), + } + ) + + # A direct answer when Google has one — saves the model a hop. + box = data.get("answerBox") or {} + answer = box.get("answer") or box.get("snippet") or None + if not answer: + kg = data.get("knowledgeGraph") or {} + answer = kg.get("description") or None + + return JSONResponse({"query": query, "answer": answer, "results": results}) + + +@app.post("/api/session") +async def session(request: Request): + """Proxy the session handshake to the load balancer, keeping its URL secret, + and meter conversation time by tier. + + The browser POSTs here (same-origin); we resolve the caller's tier, refuse if + today's budget is already spent (402), otherwise POST /session + and relay the JSON back. The LB body carries a per-session `connect_url` + (compute host + short-lived token) the browser must dial directly — that one + URL is unavoidably exposed, but the stable load-balancer address is not. On a + successful grant we reserve the first time chunk against the day's budget.""" + if not LOAD_BALANCER_URL: + # No LB configured — this deploy is direct-mode only; the browser should + # never call this. 404 so it's indistinguishable from a missing route. + raise HTTPException(status_code=404, detail="Not found.") + + tier, keys, set_cookie = auth.resolve_identity(request) + # Metering runs only on the deployed Space; off-Space the LB still proxies but + # nothing is tracked. Within metering, unlimited tiers (pro, org) aren't either. + tracked = LIMITER_ENABLED and limiter.budget_for(tier) is not None + + # Refuse before troubling the LB if the day's budget is already gone. Done + # here (at enqueue) so we never put a user who can't talk into the queue. + if tracked: + rem = await asyncio.to_thread(limiter.remaining, keys, tier) + if rem is not None and rem <= 0: + resp = JSONResponse( + {"tier": tier, "reason": "limit", "remainingSec": 0}, status_code=402 + ) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + url = f"{LOAD_BALANCER_URL.rstrip('/')}/session" + try: + async with httpx.AsyncClient(timeout=15.0) as http: + lb = await http.post(url, headers={"Content-Type": "application/json"}, content="{}") + except httpx.RequestError as exc: + logger.warning("Load balancer unreachable: %r", exc) + raise HTTPException(status_code=502, detail="Speech service unreachable.") + + # The queue is full: the LB replies 503 {state:"at_capacity"}. Relay it as-is + # so the client shows a soft "try again shortly", not a hard error. + if lb.status_code == 503: + body = _safe_json(lb) + if body.get("state") == "at_capacity": + resp = JSONResponse({"state": "at_capacity"}, status_code=503) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + if lb.status_code != 200: + # The LB's error body may name the reason (e.g. capacity); it carries no + # secret, so relay a trimmed copy. + logger.warning("Session handshake failed %s: %s", lb.status_code, lb.text[:300]) + raise HTTPException(status_code=502, detail=f"Session handshake failed ({lb.status_code}).") + + data = lb.json() + + # Busy pool: the LB queued us. Relay the ticket untouched — crucially with NO + # reservation, so waiting in line never costs the day's budget. + if data.get("state") == "queued": + data["tier"] = tier + resp = JSONResponse(data) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + # A slot was free: reserve the first chunk now and return the grant. + return await _finalize_grant(data, keys, tier, tracked, set_cookie) + + +@app.get("/api/queue/{queue_id}") +async def queue_status(queue_id: str, request: Request): + """Poll a waiting ticket: relay the position, or — when the head of the line + claims a freed slot — reserve the budget now and return the grant. Re-checks the + daily budget at claim, since a multi-minute wait could have spent it elsewhere.""" + if not LOAD_BALANCER_URL: + raise HTTPException(status_code=404, detail="Not found.") + + tier, keys, set_cookie = auth.resolve_identity(request) + tracked = LIMITER_ENABLED and limiter.budget_for(tier) is not None + + url = f"{LOAD_BALANCER_URL.rstrip('/')}/queue/{queue_id}" + try: + async with httpx.AsyncClient(timeout=15.0) as http: + lb = await http.get(url) + except httpx.RequestError as exc: + logger.warning("Load balancer unreachable: %r", exc) + raise HTTPException(status_code=502, detail="Speech service unreachable.") + + if lb.status_code == 404: + # Ticket unknown/expired (reaped after we stopped polling). Tell the client + # to start over rather than spin. + resp = JSONResponse({"state": "expired"}, status_code=404) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + if lb.status_code != 200: + logger.warning("Queue poll failed %s: %s", lb.status_code, lb.text[:300]) + raise HTTPException(status_code=502, detail=f"Queue poll failed ({lb.status_code}).") + + data = lb.json() + + if data.get("state") == "queued": + data["tier"] = tier + resp = JSONResponse(data) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + # Claimed a slot. Re-check the budget: it may have been spent in another tab + # during the wait. If so, refuse — the just-claimed slot is now a pending + # session on the LB and its pending-timeout reaper reclaims it shortly. + if tracked: + rem = await asyncio.to_thread(limiter.remaining, keys, tier) + if rem is not None and rem <= 0: + resp = JSONResponse( + {"tier": tier, "reason": "limit", "remainingSec": 0}, status_code=402 + ) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + return await _finalize_grant(data, keys, tier, tracked, set_cookie) + + +@app.delete("/api/queue/{queue_id}") +async def queue_leave(queue_id: str): + """Leave the queue from the explicit 'Leave queue' button (a real fetch).""" + if not LOAD_BALANCER_URL: + raise HTTPException(status_code=404, detail="Not found.") + await _lb_leave(queue_id) + return {"ok": True} + + +@app.post("/api/queue/end") +async def queue_end(request: Request): + """Leave the queue on teardown/tab-close (navigator.sendBeacon, which can only + POST). Body: { queueId }. Best-effort; the LB reaps the ticket on TTL anyway.""" + if not LOAD_BALANCER_URL: + raise HTTPException(status_code=404, detail="Not found.") + qid = await _queue_id(request) + if qid: + await _lb_leave(qid) + return {"ok": True} + + +async def _finalize_grant(data, keys, tier, tracked, set_cookie): + """Shared grant tail (fast path or queue claim): reserve the first chunk, attach + the metering fields the client needs, and set the anon cookie.""" + remaining = None + if tracked and data.get("session_id"): + await asyncio.to_thread(limiter.begin, data["session_id"], keys, tier) + remaining = await asyncio.to_thread(limiter.remaining, keys, tier) + + data.update({ + "tier": tier, + "limited": tracked, + "remainingSec": remaining, + "heartbeatSec": limiter.HEARTBEAT_SEC, + }) + resp = JSONResponse(data) + if set_cookie: + auth.set_anon_cookie(resp, set_cookie) + return resp + + +async def _lb_leave(queue_id: str) -> None: + """Best-effort: tell the LB to drop a waiting ticket.""" + url = f"{LOAD_BALANCER_URL.rstrip('/')}/queue/{queue_id}" + try: + async with httpx.AsyncClient(timeout=5.0) as http: + await http.delete(url) + except httpx.RequestError as exc: + logger.warning("Queue leave failed: %r", exc) + + +def _safe_json(response) -> dict: + try: + body = response.json() + except Exception: + return {} + return body if isinstance(body, dict) else {} + + +async def _queue_id(request: Request) -> str: + """Pull `queueId` from a JSON body, tolerating sendBeacon's blob posts.""" + try: + data = await request.json() + except Exception: + return "" + return (data or {}).get("queueId", "") if isinstance(data, dict) else "" + + +async def _session_id(request: Request) -> str: + """Pull `sessionId` from a JSON body, tolerating sendBeacon's blob posts.""" + try: + data = await request.json() + except Exception: + return "" + return (data or {}).get("sessionId", "") if isinstance(data, dict) else "" + + +@app.post("/api/session/heartbeat") +async def session_heartbeat(request: Request): + """Extend the live reservation one chunk at a time. `expired` once the day's + budget is spent — the client then tears down.""" + if not LIMITER_ENABLED: + raise HTTPException(status_code=404, detail="Not found.") + sid = await _session_id(request) + alive = bool(sid) and await asyncio.to_thread(limiter.heartbeat, sid) + return {"expired": not alive} + + +@app.post("/api/session/end") +async def session_end(request: Request): + """Clean teardown: reconcile to real elapsed time and refund the unused + chunk. Sent via navigator.sendBeacon, so it must succeed without a response.""" + if not LIMITER_ENABLED: + raise HTTPException(status_code=404, detail="Not found.") + sid = await _session_id(request) + if sid: + await asyncio.to_thread(limiter.end, sid) + return {"ok": True} + + +# Static front-end. Registered last so the /api routes win. `html=True` serves +# index.html at "/". The repo is public anyway, so serving the dir is fine. +app.mount("/", StaticFiles(directory=HERE, html=True), name="static") diff --git a/backend/vendor/s2s-cloud/demo/server.py.rej b/backend/vendor/s2s-cloud/demo/server.py.rej new file mode 100644 index 0000000..01af578 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/server.py.rej @@ -0,0 +1,12 @@ +@@ -47,9 +47,9 @@ + import os + from urllib.parse import urlsplit, urlunsplit + +-import auth ++from . import auth + import httpx +-import limiter ++from . import limiter + from fastapi import FastAPI, HTTPException, Request, Response + from fastapi.responses import JSONResponse + from fastapi.staticfiles import StaticFiles diff --git a/backend/vendor/s2s-cloud/demo/style.css b/backend/vendor/s2s-cloud/demo/style.css new file mode 100644 index 0000000..3df6db0 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/style.css @@ -0,0 +1,2773 @@ +:root { + --bg: #0a0b10; + --bg-elev: #13151c; + --bg-elev-2: #1b1e29; + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.16); + --text: #f5f6fa; + --text-dim: rgba(245, 246, 250, 0.65); + --text-faint: rgba(245, 246, 250, 0.42); + + --accent: #8b7dff; + --accent-2: #22d3ee; + --listening: #22d3ee; + --speaking: #8b7dff; + --processing: #f59e0b; + --error: #ff6a75; + --success: #34d399; + + /* Mono is the "machine voice": reserved for system text the app emits — + * the orb's status caption, tool calls, the transport tag. Body/UI stays + * Inter. */ + --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + + /* Thesis: color belongs to the voice. The chrome is monochrome; saturated + * hue appears only on the orb and on these tiny role echoes, which mirror + * the orb's own state colors (you listening = cyan, assistant = violet, + * tool = amber) so the transcript reads in the same color language. */ + --voice-user: var(--accent-2); + --voice-assistant: var(--accent); + --voice-tool: var(--processing); + + /* Smoothed mic RMS in [0..1], updated every frame from JS while a session + * is active. Used by audio-reactive circle states. */ + --audio-level: 0; + /* Five log-spaced frequency bands extracted from the mic analyser; + * drive each bar's height independently for a real "spectrum" feel. */ + --bar0: 0; + --bar1: 0; + --bar2: 0; + --bar3: 0; + --bar4: 0; + + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 22px; + + --shadow-soft: 0 10px 40px rgba(0, 0, 0, 0.35); + + color-scheme: dark; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + height: 100%; +} + +body { + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: radial-gradient(ellipse at 50% 20%, #1a1c28 0%, var(--bg) 60%); + color: var(--text); + min-height: 100vh; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; +} + +button { + font-family: inherit; +} + +a { + color: var(--text); + text-decoration: none; + border-bottom: 1px solid var(--border-strong); +} +a:hover { + border-bottom-color: var(--text); +} + +#app { + position: relative; + z-index: 1; + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100vh; +} + +.hidden { + display: none !important; +} + +/* ─── Topbar ──────────────────────────────────────────────────────────── */ + +/* Topbar is just a floating row of controls over the stage - no + * separator line, no background. Same story for the footer. Keeps the + * app feeling like one continuous canvas. */ +.topbar { + display: flex; + /* Top-align so the right control cluster sits up with the title instead of + * floating to the vertical middle of the tall identity block. */ + align-items: flex-start; + justify-content: space-between; + padding: 18px 28px; + /* Default stacking (below the conversation panel, z 200) so the panel drawer + * covers the controls when it's open. */ +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + font-size: 14px; + letter-spacing: 0.01em; + color: var(--text-dim); +} + +/* Transport tag rides the wordmark as a system label, not prose — mono, + * dimmed, the only mono in the topbar. */ +.brand-tag { + font-family: var(--font-mono); + font-weight: 500; + font-size: 11px; + letter-spacing: 0.02em; + opacity: 0.55; +} + +.brand .brand-logo { + width: 26px; + height: 26px; + object-fit: contain; + flex: none; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45)); + transition: transform 0.2s ease; +} +.brand .brand-logo:hover { + transform: translateY(-1px) rotate(-2deg); +} + +/* Narrow viewports (typically the mobile shell iframe and phone-sized + browser windows): the full product name pushes the topbar's right + cluster off-screen. Keep the logo as the brand cue and drop the + wordmark - the orb already gives enough context. */ +@media (max-width: 600px) { + /* The full identity stack is too tall for a phone topbar. Show only the + * title + (i); the popup carries the blurb, credits and pipeline. + * `.brand` prefix raises specificity so this beats the later base + * `.ident-meta { display: flex }` rule regardless of source order. */ + .brand .ident-blurb, + .brand .ident-meta { + display: none; + } + .ident-title { + font-size: 18px; + } + .about-btn-mobile { + display: inline-flex; + } + /* Keep phone icons at the current compact size — the desktop bump above + * shouldn't leak into the mobile topbar. */ + .topbar-right .icon-btn { + width: 36px; + height: 36px; + } + .topbar-right .icon-btn svg { + width: 18px; + height: 18px; + } + /* Keep the account control compact on phones: avatar-only chip, shorter pill. */ + .account-chip { + height: 36px; + padding: 0 6px; + } + .account-handle { + display: none; + } + .signin-pill { + height: 36px; + padding: 0 12px; + } +} + +.topbar-right { + display: flex; + align-items: center; + gap: 10px; +} + +/* The HF user pill: compact avatar + handle. Rendered as a flex row so + * the avatar stays aligned with the text baseline even when the text + * wraps on narrow viewports. */ +.hf-user { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text-dim); + padding: 4px 10px 4px 4px; + border-radius: 999px; + background: var(--bg-elev); + border: 1px solid var(--border); + line-height: 1; +} + +.hf-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; + background: color-mix(in srgb, var(--accent) 25%, var(--bg-elev-2)); + /* Hidden until an actual URL loads (see `setHfAvatar` in main.ts). + * Keeps the initial login flash from showing a broken image icon. */ + opacity: 0; + transition: opacity 0.25s ease; + flex: none; +} +.hf-avatar.loaded { + opacity: 1; +} + +.hf-user-name { + white-space: nowrap; +} + +/* ─── Transport pill ────────────────────────────────────────────── + * Surface the actual network path WebRTC picked for the robot peer + * connection (LAN, direct-through-NAT, or TURN-relayed). Lets the + * user spot at a glance when audio is going through the internet + * instead of staying on-prem. + */ +.transport-pill { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.02em; + padding: 5px 10px; + border-radius: 999px; + background: var(--bg-elev); + border: 1px solid var(--border); + color: var(--text-dim); + user-select: none; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease; +} + +.transport-pill .transport-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: currentColor; + box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 25%, transparent); + flex: none; +} + +/* Bitrate readout sits after the kind label. Dimmer + monospace so the + * kind (LAN / Direct / Relayed) stays the primary info and the numbers + * don't wiggle the layout as digits change. */ +.transport-pill .transport-bitrate { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + color: color-mix(in srgb, currentColor 75%, var(--text-dim)); + opacity: 0.85; + padding-left: 6px; + border-left: 1px solid color-mix(in srgb, currentColor 25%, transparent); +} +.transport-pill .transport-bitrate:empty { + display: none; +} + +.transport-pill.transport-checking { + color: var(--text-dim); +} +.transport-pill.transport-checking .transport-dot { + animation: transport-blink 1.2s ease-in-out infinite; +} + +.transport-pill.transport-lan { + color: #4ade80; + border-color: color-mix(in srgb, #4ade80 35%, var(--border)); + background: color-mix(in srgb, #4ade80 10%, var(--bg-elev)); +} + +.transport-pill.transport-direct { + color: #60a5fa; + border-color: color-mix(in srgb, #60a5fa 35%, var(--border)); + background: color-mix(in srgb, #60a5fa 10%, var(--bg-elev)); +} + +.transport-pill.transport-relay { + color: #fbbf24; + border-color: color-mix(in srgb, #fbbf24 35%, var(--border)); + background: color-mix(in srgb, #fbbf24 10%, var(--bg-elev)); +} + +@keyframes transport-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} + +.icon-btn { + background: var(--bg-elev); + border: 1px solid var(--border); + color: var(--text-dim); + width: 36px; + height: 36px; + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + touch-action: manipulation; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} +.icon-btn:hover { + background: var(--bg-elev-2); + color: var(--text); + border-color: var(--border-strong); +} +/* Topbar control cluster reads a touch bigger on desktop. Phones keep the + * compact 36px (reset in the mobile media query). */ +.topbar-right .icon-btn { + width: 40px; + height: 40px; +} +.topbar-right .icon-btn svg { + width: 20px; + height: 20px; +} + +/* ─── Account (HF login chip + sign-in pill + popover) ─────────────────────── */ +.account { + position: relative; + display: inline-flex; + align-items: center; +} +/* Signed-out: a compact pill that reads as the primary affordance. */ +.signin-pill { + display: inline-flex; + align-items: center; + gap: 7px; + height: 40px; + padding: 0 14px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-strong); + background: var(--bg-elev); + color: var(--text); + font-size: 13px; + font-weight: 600; + text-decoration: none; + transition: background 0.15s, border-color 0.15s; +} +.signin-pill svg { + width: 16px; + height: 16px; + flex: none; +} +.signin-pill:hover { + background: var(--bg-elev-2); + border-color: var(--text); +} +/* Narrow viewports: collapse the pill to an icon-only square. */ +@media (max-width: 800px) { + .signin-pill { + gap: 0; + width: 40px; + padding: 0; + justify-content: center; + } + .signin-pill span { + display: none; + } +} +/* Signed-in: avatar + handle chip. */ +.account-chip { + display: inline-flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 10px 0 6px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--bg-elev); + color: var(--text-dim); + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} +.account-chip:hover { + background: var(--bg-elev-2); + color: var(--text); + border-color: var(--border-strong); +} +.account-avatar { + width: 26px; + height: 26px; + border-radius: 50%; + object-fit: cover; + flex: none; +} +.account-avatar-fallback { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--bg-elev-2); + color: var(--text); + font-size: 12px; + font-weight: 700; +} +.account-handle { + font-size: 13px; + font-weight: 600; + max-width: 12ch; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.account-pro { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + padding: 2px 5px; + border-radius: 5px; + background: var(--text); + color: var(--bg); +} +/* Org "Team" badge: same pill as PRO, accent-coloured so it reads as + * unlimited without claiming a paid PRO subscription. */ +.account-team { + background: var(--accent); + color: var(--bg); +} +.account-pop { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 200px; + padding: 6px; + border-radius: var(--radius-md); + border: 1px solid var(--border-strong); + background: var(--bg-elev); + box-shadow: var(--shadow-soft); + z-index: 50; +} +.account-pop[hidden] { + display: none; +} +.account-pop-row { + padding: 8px 10px; +} +.account-pop-name { + font-weight: 600; + font-size: 13px; +} +.account-pop-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding-top: 0; + font-size: 12px; + color: var(--text-dim); +} +.account-tier { + font-weight: 600; + color: var(--text); +} +.account-pop-link { + display: block; + padding: 8px 10px; + border-radius: var(--radius-sm); + color: var(--text-dim); + font-size: 13px; + text-decoration: none; + transition: background 0.15s, color 0.15s; +} +.account-pop-link:hover { + background: var(--bg-elev-2); + color: var(--text); +} +.account-signout { + border-top: 1px solid var(--border); + margin-top: 4px; + padding-top: 10px; + border-radius: 0 0 var(--radius-sm) var(--radius-sm); +} + +/* ─── Daily-limit modal ────────────────────────────────────────────────────── */ +.limit-modal { + width: min(400px, 92vw); +} +.limit-card { + position: relative; + align-items: center; + text-align: center; + gap: 14px; + padding: 34px 28px 26px; +} +.limit-close { + position: absolute; + top: 12px; + right: 12px; +} +/* HF smiling face (brand yellow) on a neutral badge — friendly, not a stop sign. + * The logo is the single pop of colour, so the badge itself stays quiet. */ +.limit-badge { + width: 66px; + height: 66px; + border-radius: 50%; + display: grid; + place-items: center; + margin: 2px auto 2px; + background: var(--bg-elev-2); + border: 1px solid var(--border-strong); + box-shadow: 0 0 0 6px rgba(255, 210, 30, 0.06); +} +.limit-badge .hf-logo { + width: 46px; + height: auto; +} +.limit-title { + margin: 0; + font-size: 20px; + font-weight: 700; + letter-spacing: 0; +} +.limit-msg { + color: var(--text-dim); + font-size: 14px; + line-height: 1.55; + margin: 0; + max-width: 30ch; +} +.limit-cta { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 4px; +} +.limit-cta .hf-logo { + width: 20px; + height: auto; + flex: none; +} +.limit-note { + margin: 0; + font-size: 12px; + color: var(--text-faint); +} +#limit-cta[hidden] { + display: none; +} + +/* ─── Stage ───────────────────────────────────────────────────────────── */ + +.stage { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px 24px 32px; + gap: 20px; + min-height: 0; + background: transparent; +} + +/* ─── Central circle ──────────────────────────────────────────────────── */ + +/* Wraps the orb and its two side controls so they stay aligned on one row. */ +.orb-wrap { + position: relative; + display: flex; + align-items: center; + justify-content: center; + gap: clamp(18px, 3vw, 32px); +} + +.circle { + position: relative; + /* Cap by height as well as width (min(vw, vh)-style): in a short container + * the orb shrinks instead of overflowing, so the page stays scroll-free even + * when embedded in a small box. On normal screens the 320px cap still wins, + * so the desktop size is unchanged. */ + width: clamp(140px, min(38vw, 46vh), 320px); + aspect-ratio: 1 / 1; + border-radius: 50%; + border: none; + background: transparent; + padding: 0; + cursor: pointer; + outline: none; + display: grid; + place-items: center; + color: var(--glow, var(--accent)); + transition: transform 0.18s ease, filter 0.18s ease; + -webkit-tap-highlight-color: transparent; + /* Treat taps as immediate clicks (no 300ms delay / double-tap-zoom). */ + touch-action: manipulation; +} +.circle:hover { + filter: brightness(1.08); +} +.circle:active { + transform: scale(0.97); + filter: brightness(0.92); +} +.circle:focus-visible .circle-core { + outline: 2px solid var(--glow, var(--accent)); + outline-offset: 6px; +} +.circle[disabled] { + cursor: default; + opacity: 0.75; +} + +.circle-glow { + position: absolute; + inset: 0; + border-radius: 50%; + background: radial-gradient(circle at center, var(--glow, var(--accent)) 0%, transparent 65%); + filter: blur(28px); + opacity: 0.5; + transform: scale(1); + transition: opacity 0.25s, background 0.25s, transform 1.4s ease-in-out; + pointer-events: none; +} + +/* Two nested circular rings: the inner tracks the core edge, the outer + * expands / fades to convey "audio radiating out" during speaking. */ +.circle-ring, +.circle-ring-outer { + position: absolute; + top: 50%; + left: 50%; + border-radius: 50%; + transform: translate(-50%, -50%); + pointer-events: none; + transition: opacity 0.4s ease, border-color 0.4s ease, transform 0.3s ease; +} +.circle-ring { + width: 82%; + height: 82%; + border: 1.5px solid color-mix(in srgb, var(--glow, var(--accent)) 35%, transparent); + opacity: 0.35; +} +.circle-ring-outer { + width: 94%; + height: 94%; + border: 1px solid color-mix(in srgb, var(--glow, var(--accent)) 22%, transparent); + opacity: 0; +} + +.circle-core { + position: relative; + width: 72%; + height: 72%; + border-radius: 50%; + background: radial-gradient( + circle at 35% 28%, + color-mix(in srgb, var(--glow, var(--accent)) 28%, transparent), + color-mix(in srgb, var(--glow, var(--accent)) 10%, transparent) 55%, + color-mix(in srgb, var(--glow, var(--accent)) 5%, transparent) + ); + border: 2px solid color-mix(in srgb, var(--glow, var(--accent)) 40%, transparent); + display: grid; + place-items: center; + box-shadow: + 0 0 32px color-mix(in srgb, var(--glow, var(--accent)) 22%, transparent), + inset 0 0 28px color-mix(in srgb, var(--glow, var(--accent)) 18%, transparent); + transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease, transform 0.4s ease; +} + +/* Indicator slot: a single SVG / spinner / bar group is visible at a time, + * driven by the state class on `.circle`. */ +.circle-indicator { + position: relative; + width: 44%; + height: 44%; + display: grid; + place-items: center; + color: var(--glow, var(--accent)); +} +.circle-indicator > .ind { + grid-area: 1 / 1; + opacity: 0; + transform: scale(0.85); + transition: opacity 0.25s ease, transform 0.25s ease; + pointer-events: none; +} +.circle-indicator > svg.ind { + width: 60%; + height: 60%; +} + +/* Spinner: a rotating ring gap, CSS-only. + * + * Note: the base `.circle-indicator > .ind` rule forces `transform: + * scale(.85)` / `scale(1)` on every indicator to drive the show/hide + * transition. If we only rotate here, the browser has to interpolate + * between `scale(1)` and `rotate(360deg)` (two different transform + * functions), which produces a broken, barely-moving animation. So we + * include the scale explicitly in the keyframes and bump specificity + * with `!important` so the spinner always wins over the state rule. */ +.ind-spinner { + width: 48%; + height: 48%; + border: 3px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + opacity: 0; + animation: ind-spin 0.9s linear infinite; +} + +/* Thinking dots: 3 soft pulsing dots while the model is composing a + * response. Apple-style cadence: each dot scales up and brightens in + * turn, staggered by ~160 ms. Per-dot animation is on the child, so + * the parent's scale(.85 → 1) show/hide transform composes cleanly. */ +.ind-thinking { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 60%; + height: 60%; +} +.ind-thinking .dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + opacity: 0.3; + animation: thinking-dot 1.25s ease-in-out infinite; +} +.ind-thinking .dot:nth-child(1) { animation-delay: 0s; } +.ind-thinking .dot:nth-child(2) { animation-delay: 0.16s; } +.ind-thinking .dot:nth-child(3) { animation-delay: 0.32s; } + +/* Bars: 5 vertical pills driven by --bar0..--bar4 CSS vars. */ +.ind-bars { + display: flex; + align-items: center; + gap: 5px; + height: 42%; +} +.ind-bars .bar { + width: 4px; + min-height: 4px; + border-radius: 3px; + background: currentColor; + opacity: 0.7; + --h: var(--bar0, 0); + height: calc(4px + var(--h) * 36px); + transition: height 0.08s ease-out, opacity 0.08s ease-out; +} +.ind-bars .bar:nth-child(1) { --h: var(--bar0); } +.ind-bars .bar:nth-child(2) { --h: var(--bar1); } +.ind-bars .bar:nth-child(3) { --h: var(--bar2); } +.ind-bars .bar:nth-child(4) { --h: var(--bar3); } +.ind-bars .bar:nth-child(5) { --h: var(--bar4); } +.ind-bars .bar { + opacity: calc(0.55 + 0.45 * var(--h)); +} + +/* Active indicator per state. + * + * Note: `ai-speaking` deliberately has NO indicator here. The orb + * itself becomes the indicator by pulsing on Reachy's voice (see the + * `--ai-audio-level` rules further down), which is a lot clearer and + * less confusing than reusing the mic-bars (which viewers would read + * as "you are speaking"). */ +.state-signed-out .ind-connect, +.state-authenticated .ind-mic, +.state-ready .ind-mic, +.state-connecting .ind-spinner, +.state-connected .ind-spinner, +.state-auto-selecting .ind-spinner, +.state-starting .ind-spinner, +.state-queued .ind-spinner, +.state-processing .ind-thinking, +.state-listening .ind-bars, +.state-user-speaking .ind-bars, +.state-ai-speaking .ind-voice, +.state-error .ind-error { + opacity: 1; + transform: scale(1); +} + +/* AI speaking indicator: speaker + two sound waves. + * + * Each wave pulses outward (opacity + stroke grow) with a quarter- + * beat offset so it reads as sound radiating out. Kept as a pure + * CSS animation so the icon is always visually alive even between + * syllables when --ai-audio-level momentarily dips. */ +.ind-voice .wave { + transform-origin: 50% 50%; + animation: voice-wave 1.35s ease-out infinite; +} +.ind-voice .wave-1 { animation-delay: 0s; } +.ind-voice .wave-2 { animation-delay: 0.35s; } + +/* Idle indicators: a chain-link "connect" glyph for the signed-out + * step (invites the user to authenticate with HF) and a microphone + * once a session is ready. Both picked up by the generic + * `.circle-indicator > svg.ind` sizing rule (60% × 60% of the slot) + * and rely on per-state opacity transitions for show / hide. */ +.ind-connect, +.ind-mic { + color: color-mix(in srgb, var(--glow, var(--accent)) 85%, white); +} + +/* ─── Caption below the circle ────────────────────────────────────────── */ + +/* The caption under the orb is meant to whisper, not shout: micro-label + * vibe, uppercase, letter-spaced, muted. Only appears for actionable / + * transitional states (see STATE_VIEWS). During a live conversation the + * orb alone carries the state so we collapse this row entirely. */ +.circle-caption { + margin: 0; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #1a0e13; + min-height: 1.2em; + text-align: center; + opacity: 0.75; + transition: opacity 0.25s ease, color 0.25s ease, transform 0.25s ease, + min-height 0.25s ease; +} +.circle-caption.empty { + opacity: 0; + min-height: 0; + transform: translateY(-4px); + pointer-events: none; +} +.circle-caption.muted { + color: #1a0e13; + opacity: 0.65; +} +.circle-caption.error { + color: #1a0e13; + opacity: 1; + letter-spacing: 0.08em; +} + +/* Warm, human line under the mono caption — sentence case, only while queued. + * The caption keeps the terse position; this reassures. */ +.circle-subcaption { + margin: 8px 0 0; + max-width: 32ch; + font-size: 13.5px; + line-height: 1.5; + color: var(--text-dim); + text-align: center; + text-wrap: balance; + transition: opacity 0.25s ease; +} +.circle-subcaption[hidden] { display: none; } + +/* Queue actions sit under the caption: "Join now" (primary, only when it's your + * turn) stacked above the quiet "Leave queue" escape hatch. */ +.queue-actions { + margin-top: 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} +.queue-actions[hidden] { display: none; } + +/* "Join now": the one call to action in the queue flow, so it reads as inviting + * (filled accent) while everything around it stays quiet. */ +.join-queue-btn { + padding: 10px 26px; + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: #0b0b10; + background: var(--accent); + border: none; + border-radius: 999px; + cursor: pointer; + font-variant-numeric: tabular-nums; + transition: transform 0.12s ease, filter 0.2s ease; +} +.join-queue-btn:hover { filter: brightness(1.08); } +.join-queue-btn:active { transform: scale(0.97); } +.join-queue-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} +.join-queue-btn[hidden] { display: none; } + +/* "Leave queue": a quiet outlined pill, understated so it reads as an escape + * hatch, not a CTA. */ +.leave-queue-btn { + padding: 7px 16px; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-faint); + background: transparent; + border: 1px solid color-mix(in srgb, var(--text-faint) 35%, transparent); + border-radius: 999px; + cursor: pointer; + transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease; +} +.leave-queue-btn:hover { + color: var(--text); + border-color: color-mix(in srgb, var(--text-faint) 60%, transparent); + background: color-mix(in srgb, var(--text-faint) 8%, transparent); +} +.leave-queue-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.leave-queue-btn[hidden] { display: none; } + +/* ─── Tool-call toaster ───────────────────────────────────────────────── */ + +/* A small, non-interactive pill that appears below the circle when the + * model invokes a tool (move_head, play_move). Sits just under the + * state caption, collapses to zero height when no toast is active. */ +.tool-toast { + display: inline-flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding: 6px 12px; + border-radius: 999px; + border: 1px solid var(--border-strong); + background: color-mix(in srgb, var(--bg-elev-2) 78%, transparent); + color: var(--text-dim); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.01em; + line-height: 1; + white-space: nowrap; + max-width: 80vw; + overflow: hidden; + text-overflow: ellipsis; + + opacity: 0; + transform: translateY(-4px) scale(0.96); + pointer-events: none; + transition: opacity 0.22s ease, transform 0.22s ease; +} +.tool-toast.visible { + opacity: 1; + transform: translateY(0) scale(1); +} +.tool-toast-icon { + width: 14px; + height: 14px; + flex: none; + color: color-mix(in srgb, var(--voice-tool) 80%, white); + animation: tool-toast-spin 3.2s linear infinite; + animation-play-state: paused; +} +.tool-toast.visible .tool-toast-icon { + animation-play-state: running; +} +.tool-toast-text { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; +} +@keyframes tool-toast-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* ─── Side controls (mic / stop) ──────────────────────────────────────── */ + +/* Mic button + its radial noise-gate arc. The wrapper centres the button; the + arc SVG is an absolute overlay larger than the button, revealed with the + live session (it has no layout footprint, so the idle collapse is unaffected). */ +.mic-gate { + position: relative; + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; +} +.mic-gate-arc { + position: absolute; + left: 50%; + top: 50%; + width: 80px; + height: 80px; + transform: translate(-50%, -50%); + pointer-events: none; /* only the hit-path below catches drags */ + opacity: 0; + transition: opacity 0.25s ease; +} +.orb-wrap.live .mic-gate-arc { opacity: 1; } +.mga-track { + stroke: rgba(255, 255, 255, 0.1); /* hairline; recedes until needed */ + stroke-width: 1.5; + stroke-linecap: round; +} +.mga-fill { + stroke: var(--accent-2); + stroke-width: 2; + stroke-linecap: round; + opacity: 0.85; + transition: stroke-dashoffset 0.06s linear; +} +/* Threshold setpoint: a bead riding the ring. White with a thin dark outline so + it stays legible over the moving fill; it recolors to cyan with a soft glow + the moment the live level crosses it (the gate opening). */ +.mga-handle { + fill: var(--text); + stroke: none; + transition: fill 0.2s ease, filter 0.2s ease; +} +.mic-gate.gate-open .mga-handle { + fill: var(--accent-2); + filter: drop-shadow(0 0 3px var(--accent-2)); +} +.mga-hit { + stroke: transparent; + stroke-width: 16; + pointer-events: none; /* enabled only while live (below) */ + cursor: pointer; + touch-action: none; +} +/* Only catch drags during a live call; when idle the arc is hidden and must + not steal clicks near the collapsed mic button / orb. */ +.orb-wrap.live .mga-hit { pointer-events: stroke; } + +.side-btn { + flex: none; + width: 52px; + height: 52px; + border-radius: 50%; + border: 1px solid var(--border-strong); + /* Brighter background so the buttons actually stand out against the + * deep-blue stage gradient; previous var(--bg-elev) was too close to + * the page bg to be readable. */ + background: color-mix(in srgb, var(--bg-elev-2) 80%, #2a2e3c); + color: var(--text); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + touch-action: manipulation; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + /* Hidden by default: take up no space until the session is live. The + * `width: 0` collapse keeps the orb centered on the idle screen. */ + opacity: 0; + transform: scale(0.55); + width: 0; + padding: 0; + pointer-events: none; + overflow: hidden; + transition: opacity 0.25s ease, transform 0.25s ease, width 0.25s ease, + background 0.15s, color 0.15s, border-color 0.15s; +} +.side-btn:hover { + background: color-mix(in srgb, var(--bg-elev-2) 60%, #323746); + border-color: var(--text-dim); +} +.side-btn svg { + width: 22px; + height: 22px; + flex: none; +} +.side-btn .mic-off { display: none; } +.side-btn.muted { + color: #fff; + border-color: var(--error); + background: color-mix(in srgb, var(--error) 70%, #1a0e13); +} +.side-btn.muted .mic-on { display: none; } +.side-btn.muted .mic-off { display: block; } + +/* Stop button: subtle warm tint so "end" reads as destructive. */ +#stop-btn:hover { + color: #fff; + border-color: color-mix(in srgb, var(--error) 60%, var(--border-strong)); + background: color-mix(in srgb, var(--error) 25%, var(--bg-elev-2)); +} + +/* Reveal when the session is live: buttons flank the orb on a flex row. */ +.orb-wrap.live .side-btn { + opacity: 1; + transform: scale(1); + width: 52px; + pointer-events: auto; +} + +/* Disable every transition / animation during the first paint so the + * orb doesn't fade-and-scale in when the page loads. `main.ts` removes + * the class after one animation frame. */ +body.booting, +body.booting *, +body.booting *::before, +body.booting *::after { + transition: none !important; + animation-duration: 0s !important; + animation-delay: 0s !important; +} + +/* ─── Circle animation keyframes ──────────────────────────────────────── */ + +/* Slow, subtle breathing for "warm idle" states. */ +@keyframes breathe { + 0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.4; } + 50% { transform: translate(-50%, -50%) scale(1.06); opacity: 0.15; } +} + +/* Outer ring expanding and fading - conveys "I am producing audio". */ +@keyframes ring-out { + 0% { transform: translate(-50%, -50%) scale(1); opacity: 0.35; } + 100% { transform: translate(-50%, -50%) scale(1.18); opacity: 0; } +} + +/* Soft inner scale for the core while talking. */ +@keyframes core-breathe { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.04); } +} + +/* Subtle glow throb used for "thinking" — dimmer than speaking. */ +@keyframes thinking { + 0%, 100% { transform: scale(1); opacity: 0.7; } + 50% { transform: scale(0.96); opacity: 0.45; } +} + +/* Individual dot pulse for the 3-dot processing indicator. */ +@keyframes thinking-dot { + 0%, 60%, 100% { transform: scale(0.7); opacity: 0.3; } + 30% { transform: scale(1.15); opacity: 1; } +} + +/* Sound-wave pulse: arc fades in, scales up slightly, fades out. + * The transform-origin is the speaker's center (roughly x=12), so + * the waves feel like they're emanating from the cone. */ +@keyframes voice-wave { + 0% { opacity: 0; transform: scale(0.7); } + 30% { opacity: 1; transform: scale(1); } + 70% { opacity: 0.2; transform: scale(1.12); } + 100% { opacity: 0; transform: scale(0.7); } +} + +@keyframes ind-spin { + /* Scale kept at 1 so we don't fight with the indicator's base + * show/hide transform (see `.ind-spinner` for the rationale). */ + from { transform: scale(1) rotate(0deg); } + to { transform: scale(1) rotate(360deg); } +} + +/* ─── State-specific colors ──────────────────────────────────────────── */ + +.circle.state-signed-out { --glow: #8b7dff; } +.circle.state-authenticated, +.circle.state-ready { --glow: #34d399; } +.circle.state-connecting, +.circle.state-connected, +.circle.state-auto-selecting, +.circle.state-starting { --glow: #facc15; } +/* Queued: a calm slate glow, distinct from connecting's active yellow — this is + * waiting, not working. The spinner turns slowly and the ring breathes. */ +.circle.state-queued { --glow: #94a3b8; } +.circle.state-queued .ind-spinner { animation-duration: 2.4s; } +.circle.state-queued .circle-ring { animation: breathe 2.6s ease-in-out infinite; } +/* Your turn: a slot is held for you — the orb warms to the accent and breathes a + * little quicker, an invitation to join. */ +.circle.state-your-turn { --glow: var(--accent); } +.circle.state-your-turn .circle-ring { animation: breathe 1.6s ease-in-out infinite; } +.circle.state-listening, +.circle.state-user-speaking { --glow: var(--listening); } +.circle.state-processing { --glow: var(--processing); } +.circle.state-ai-speaking { --glow: var(--speaking); } +.circle.state-error { --glow: var(--error); } + +/* Idle / ready: gentle breathing of the inner ring. Kept out of the + * `signed-out` state so the very first paint on page load stays quiet + * (the orb now shows the Reachy head silhouette, no need to also pulse). */ +.circle.state-authenticated .circle-ring, +.circle.state-ready .circle-ring { + animation: breathe 2.4s ease-in-out infinite; +} + +/* Connecting flows: subtle glow throb so the orb feels thoughtful + * while the session is being negotiated. Kept off `processing` on + * purpose - the 3 thinking dots already pulse, and layering another + * throb on top competes with them for attention. */ +.circle.state-connecting .circle-core, +.circle.state-connected .circle-core, +.circle.state-auto-selecting .circle-core, +.circle.state-starting .circle-core { + animation: thinking 1.4s ease-in-out infinite; +} + +/* User is speaking: the mic RMS drives scale + opacity via --audio-level. */ +.circle.state-user-speaking .circle-ring { + animation: none; + opacity: calc(0.25 + 0.55 * var(--audio-level)); + transform: translate(-50%, -50%) scale(calc(1 + 0.08 * var(--audio-level))); + transition: transform 0.08s linear, opacity 0.08s linear; +} +.circle.state-user-speaking .circle-ring-outer { + opacity: calc(0.1 + 0.35 * var(--audio-level)); + transform: translate(-50%, -50%) scale(calc(1 + 0.12 * var(--audio-level))); + transition: transform 0.08s linear, opacity 0.08s linear; +} +.circle.state-listening .circle-ring { + animation: breathe 2s ease-in-out infinite; +} + +/* Assistant is speaking: the whole orb breathes in sync with Reachy's + * voice instead of running a fixed timer. `--ai-audio-level` (0-1) is + * updated at display rate by AiLevelMonitor from the OpenAI output + * track, so every syllable visibly moves the core + outer ring. This + * reads instantly as "the orb is the voice" and completely avoids the + * ambiguity of bars-vs-mic the user flagged. */ +.circle.state-ai-speaking .circle-core { + animation: none; + transform: scale(calc(1 + 0.09 * var(--ai-audio-level, 0))); + transition: transform 0.08s linear; +} +.circle.state-ai-speaking .circle-ring { + animation: none; + opacity: calc(0.3 + 0.5 * var(--ai-audio-level, 0)); + transform: translate(-50%, -50%) scale(calc(1 + 0.05 * var(--ai-audio-level, 0))); + transition: transform 0.08s linear, opacity 0.08s linear; +} +.circle.state-ai-speaking .circle-ring-outer { + animation: none; + opacity: calc(0.15 + 0.55 * var(--ai-audio-level, 0)); + transform: translate(-50%, -50%) scale(calc(1 + 0.18 * var(--ai-audio-level, 0))); + transition: transform 0.08s linear, opacity 0.08s linear; +} +.circle.state-ai-speaking .circle-glow { + opacity: calc(0.35 + 0.45 * var(--ai-audio-level, 0)); + transition: opacity 0.08s linear; +} + +/* ─── Robot picker ────────────────────────────────────────────────────── */ + +.robot-picker { + width: min(420px, 92vw); + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 14px 16px; + box-shadow: var(--shadow-soft); +} + +.picker-title { + margin: 0 0 10px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); +} + +.robot-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.robot-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-elev-2); + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} +.robot-card:hover { + border-color: var(--border-strong); +} +.robot-card.selected { + border-color: var(--border-strong); + background: var(--bg-elev); +} + +.robot-card .name { + font-weight: 600; + font-size: 14px; +} +.robot-card .id { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-faint); +} + +.robot-empty { + padding: 14px; + text-align: center; + color: var(--text-faint); + font-size: 13px; +} + +/* ─── Footer ──────────────────────────────────────────────────────────── */ + +/* ─── Text input bar ─────────────────────────────────────────────────────── */ + +.text-input-bar { + display: flex; + align-items: center; + gap: 8px; + max-width: 560px; + margin: 20px auto 0; + padding: 0 20px; + width: 100%; +} + +.text-input { + flex: 1; + background: var(--bg-elev-2); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + padding: 10px 16px; + font-family: inherit; + font-size: 14px; + color: var(--text); + outline: none; + transition: border-color 0.2s ease; +} + +.text-input::placeholder { + color: var(--text-faint); +} + +.text-input:focus { + border-color: var(--accent-2); +} + +.text-input:disabled { + opacity: 0.4; + cursor: not-allowed; +} + +.text-send-btn { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + border: none; + background: var(--accent); + color: #fff; + cursor: pointer; + transition: opacity 0.2s ease, background 0.2s ease; + flex-shrink: 0; +} + +.text-send-btn:hover:not(:disabled) { + background: var(--accent-2); +} + +.text-send-btn:disabled { + opacity: 0.35; + cursor: not-allowed; +} + +.footer { + padding: 12px 28px 18px; + font-size: 11px; + letter-spacing: 0.02em; + color: var(--text-faint); + display: flex; + justify-content: center; + opacity: 0.55; + transition: opacity 0.25s ease; +} +.footer:hover { + opacity: 0.9; +} +.footer a { + color: inherit; + text-decoration: none; + border-bottom: 1px dotted currentColor; +} +.footer a:hover { + color: var(--text-dim); +} +/* While the webcam preview sits bottom-left, push the credit to the + * bottom-right so the two don't collide. */ +body.cam-on .footer { + justify-content: flex-end; +} + +/* ─── Modal ───────────────────────────────────────────────────────────── */ + +.modal { + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 0; + background: var(--bg-elev); + color: var(--text); + width: min(440px, 92vw); + box-shadow: var(--shadow-soft); +} +.modal::backdrop { + background: rgba(8, 9, 13, 0.65); + backdrop-filter: blur(4px); +} + +.modal-content { + display: flex; + flex-direction: column; + gap: 16px; + padding: 22px 24px 20px; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} +.modal-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.02em; +} + +.field[hidden] { display: none; } +.field { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + font-weight: 500; + color: var(--text-dim); +} +.field > span { + color: var(--text); + font-weight: 600; + font-size: 12px; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.field input, +.field select, +.field textarea { + font-family: inherit; + font-size: 14px; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 12px; + outline: none; + transition: border-color 0.15s; + resize: vertical; +} +.field textarea { + min-height: 84px; +} +.field input:focus, +.field select:focus, +.field textarea:focus { + border-color: var(--text-dim); +} +/* Upload button for custom voice samples */ +.btn-upload { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + font-family: inherit; + font-size: 13px; + font-weight: 600; + color: var(--accent); + background: var(--bg); + border: 1px dashed var(--accent); + border-radius: var(--radius-sm); + padding: 8px 12px; + cursor: pointer; + transition: background 0.15s, color 0.15s; + user-select: none; + margin-top: 2px; +} +.btn-upload:hover { + background: var(--accent); + color: #fff; +} +.btn-upload:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +/* Deploy-pinned server URL: shown for transparency, not editable. */ +.field input[readonly] { + color: var(--text-dim); + border-style: dashed; + cursor: default; +} +.field input[readonly]:focus { + border-color: var(--border); +} +.field small { + color: var(--text-faint); + font-size: 12px; + line-height: 1.4; +} +.field small.error { color: var(--error); } +.field small code { + background: var(--bg); + padding: 1px 5px; + border-radius: 4px; + border: 1px solid var(--border); +} + +/* Noise gate: a header with a live value, a level meter, and a range slider + that shares the meter's horizontal (dB) axis. */ +.field-head { + display: flex; + align-items: baseline; + justify-content: space-between; +} +.field-value { + font-weight: 500; + font-size: 12px; + letter-spacing: 0; + text-transform: none; + color: var(--text-dim); +} +/* The slider and the level meter are one widget: the range input is overlaid + on the meter track (its native track is transparent), so the live-level fill + shows through behind the thumb and the thumb itself is the threshold. */ +.gate { + display: flex; + flex-direction: column; + gap: 4px; +} +.gate-track { + position: relative; + height: 14px; + display: flex; + align-items: center; +} +.gate-track::before { + /* the visible meter track */ + content: ""; + position: absolute; + left: 0; + right: 0; + height: 8px; + border-radius: 999px; + background: var(--bg); + border: 1px solid var(--border); +} +.gate-meter-fill { + position: absolute; + left: 1px; + top: 50%; + transform: translateY(-50%); + height: 6px; + width: 0; + border-radius: 999px; + background: var(--accent-2); + transition: width 0.06s linear; + pointer-events: none; +} +.gate-ends { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--text-faint); + letter-spacing: 0; + text-transform: none; +} +/* The range input sits transparently on top of the meter track. */ +.gate-track input[type="range"] { + position: relative; + z-index: 1; + width: 100%; + margin: 0; + -webkit-appearance: none; + appearance: none; + padding: 0; + border: none; + background: transparent; + height: 14px; + cursor: pointer; +} +.gate-track input[type="range"]::-webkit-slider-runnable-track { + height: 14px; + background: transparent; +} +.gate-track input[type="range"]::-moz-range-track { + height: 14px; + background: transparent; +} +.gate-track input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 6px; + height: 18px; + border-radius: 3px; + background: var(--text); + border: 2px solid var(--bg-elev); + box-shadow: 0 0 0 1px var(--border-strong); +} +.gate-track input[type="range"]::-moz-range-thumb { + width: 6px; + height: 18px; + border-radius: 3px; + background: var(--text); + border: 2px solid var(--bg-elev); + box-shadow: 0 0 0 1px var(--border-strong); +} +.gate-track input[type="range"]:focus { border: none; } + +.modal-footer { + display: flex; + justify-content: space-between; + gap: 12px; + padding-top: 6px; +} + +.btn { + padding: 10px 16px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-strong); + background: var(--bg-elev-2); + color: var(--text); + font-weight: 600; + font-size: 13px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, transform 0.05s; +} +.btn:hover { + border-color: var(--text); +} +.btn:active { + transform: translateY(1px); +} +.btn.primary { + background: var(--text); + border-color: var(--text); + color: var(--bg); +} +.btn.primary:hover { + background: #fff; + border-color: #fff; +} +.btn.ghost { + background: transparent; + border-color: var(--border); + color: var(--text-dim); +} +.btn.wide { + width: 100%; + padding: 12px 16px; +} +.btn[disabled] { + opacity: 0.45; + cursor: not-allowed; +} +.btn[disabled]:hover { + border-color: var(--border-strong); +} + +/* ─── Settings tabs ───────────────────────────────────────────────────── */ + +.tabs { + display: flex; + gap: 4px; + padding: 4px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} +.tab { + flex: 1; + padding: 8px 12px; + border: none; + background: transparent; + color: var(--text-dim); + font-family: inherit; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.02em; + border-radius: calc(var(--radius-sm) - 3px); + cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.tab:hover { + color: var(--text); +} +.tab.active { + background: var(--bg-elev-2); + color: var(--text); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.tab-panels { + display: flex; + flex-direction: column; +} +.tab-panel { + display: flex; + flex-direction: column; + gap: 16px; +} +.tab-panel[hidden] { + display: none; +} + +/* Horizontal layout for Voice + Model. */ +.field-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +/* ─── Chat button + badge ────────────────────────────────────────────── */ + +#chat-btn { + position: relative; +} + +.chat-badge { + position: absolute; + top: 6px; + right: 6px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text); + border: 1.5px solid var(--bg-elev); + opacity: 0; + transform: scale(0); + transition: opacity 0.2s ease, transform 0.2s ease; + pointer-events: none; +} +.chat-badge.visible { + opacity: 1; + transform: scale(1); +} + +/* ─── Lyric-style ephemeral transcript ───────────────────────────────── + * A vertical lyrics stack like a music player: newest line is largest and + * sits at the bottom, older lines shrink and fade as they move up, and the + * oldest fades out at the top as new lines arrive. */ + +.bubble-stack { + position: fixed; + right: 40px; + bottom: 15%; + width: min(440px, calc(100vw - 64px)); + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: flex-end; + gap: 6px; + pointer-events: none; + z-index: 100; +} + +.bubble { + pointer-events: none; + max-width: 100%; + padding: 2px 0; + text-align: right; + line-height: 1.35; + word-break: break-word; + font-size: 12px; + font-weight: 400; + color: rgba(255, 255, 255, 0.85); + opacity: 0; + transform: translateY(12px); + transition: opacity 0.25s ease, transform 0.25s ease, font-size 0.25s ease; +} +.bubble.in { + opacity: 1; + transform: translateY(0); +} +.bubble.out { + opacity: 0; + transform: translateY(-10px); + transition: opacity 0.3s ease, transform 0.3s ease; +} + +/* Progressive lyric sizing — newest line (last child) is biggest, older + * lines shrink and dim as they climb the stack. */ +.bubble.in:last-child { + font-size: 26px; + font-weight: 700; +} +.bubble.in:nth-last-child(2) { + font-size: 21px; + font-weight: 600; + opacity: 0.85; +} +.bubble.in:nth-last-child(3) { + font-size: 17px; + font-weight: 500; + opacity: 0.7; +} +.bubble.in:nth-last-child(4) { + font-size: 14px; + font-weight: 400; + opacity: 0.55; +} +.bubble.in:nth-last-child(n+5) { + font-size: 12px; + font-weight: 400; + opacity: 0.35; +} + +.bubble.user, +.bubble.assistant, +.bubble.tool { + align-self: flex-end; +} +.bubble.user { + color: rgba(190, 235, 251, 0.82); +} +.bubble.assistant { + color: rgba(214, 208, 255, 0.82); +} +.bubble.tool { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + color: rgba(255, 255, 255, 0.5); + font-family: var(--font-mono); + font-size: 12px !important; + font-weight: 400 !important; + opacity: 0.7; +} +.bubble.tool .bubble-tool-icon { + width: 13px; + height: 13px; + flex: none; + color: var(--voice-tool); +} + +.bubble-role { + display: none; +} + +/* ─── Conversation history panel ─────────────────────────────────────── */ + +.chat-panel { + position: fixed; + inset: 0; + z-index: 200; + pointer-events: none; +} +.chat-panel-backdrop { + position: absolute; + inset: 0; + background: rgba(8, 9, 13, 0.45); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + opacity: 0; + transition: opacity 0.25s ease; +} +.chat-panel.open .chat-panel-backdrop { + opacity: 1; + pointer-events: auto; +} +.chat-panel-inner { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: min(360px, 90vw); + background: var(--bg-elev); + border-left: 1px solid var(--border); + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1); + pointer-events: auto; + box-shadow: -8px 0 32px rgba(0, 0, 0, 0.35); +} +.chat-panel.open .chat-panel-inner { + transform: translateX(0); +} +.chat-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 20px; + border-bottom: 1px solid var(--border); + flex: none; +} +.chat-panel-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.01em; +} +.chat-history { + flex: 1; + overflow-y: auto; + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 10px; + scroll-behavior: smooth; +} +.chat-history::-webkit-scrollbar { width: 3px; } +.chat-history::-webkit-scrollbar-track { background: transparent; } +.chat-history::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } + +.chat-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + color: var(--text-faint); + padding: 48px 0; + opacity: 0.65; +} +.chat-empty svg { + margin-bottom: 6px; + opacity: 0.8; +} +.chat-empty-title { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; +} +.chat-empty-hint { + font-size: 12px; + color: var(--text-faint); + opacity: 0.7; +} + +/* ─── History messages ───────────────────────────────────────────────── */ + +.hist-msg { + display: flex; + flex-direction: column; + gap: 3px; + max-width: 88%; +} +.hist-msg.user { align-self: flex-start; } +.hist-msg.assistant { align-self: flex-end; } +.hist-msg.tool { align-self: flex-start; max-width: 100%; } + +.hist-role { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); + padding: 0 2px; +} +.hist-msg.user .hist-role { color: color-mix(in srgb, var(--voice-user) 75%, var(--text-faint)); } +.hist-msg.assistant .hist-role { color: color-mix(in srgb, var(--voice-assistant) 75%, var(--text-faint)); } +.hist-msg.tool .hist-role { color: color-mix(in srgb, var(--voice-tool) 75%, var(--text-faint)); } + +.hist-body { + padding: 9px 12px; + border-radius: var(--radius-md); + font-size: 13px; + line-height: 1.5; + border: 1px solid var(--border); + background: var(--bg-elev-2); + word-break: break-word; +} +/* Bodies share one neutral surface; alignment + the mono role label do the + * distinguishing, so the panel reads as one quiet column. */ +.hist-msg.user .hist-body.partial { opacity: 0.65; } + +/* ─── Tool call history item ─────────────────────────────────────────── */ + +.hist-tool-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: var(--radius-md) var(--radius-md) 0 0; + background: color-mix(in srgb, var(--processing) 10%, var(--bg-elev-2)); + border: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border)); + border-bottom: 1px solid color-mix(in srgb, var(--processing) 15%, var(--border)); + cursor: pointer; + color: var(--text); + font-family: inherit; + font-size: 13px; + font-weight: 500; + width: 100%; + text-align: left; + transition: background 0.15s; +} +.hist-tool-header:only-child { + border-radius: var(--radius-md); + border-bottom: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border)); +} +.hist-tool-header:hover { + background: color-mix(in srgb, var(--processing) 16%, var(--bg-elev-2)); +} +.hist-tool-icon { + width: 13px; + height: 13px; + flex: none; + color: var(--processing); +} +.hist-tool-name { + font-family: var(--font-mono); + font-size: 12px; + color: var(--voice-tool); + font-weight: 600; +} +.hist-tool-chevron { + margin-left: auto; + width: 13px; + height: 13px; + color: var(--text-faint); + transition: transform 0.2s ease; + flex: none; +} +.hist-tool-header[aria-expanded="true"] .hist-tool-chevron { + transform: rotate(180deg); +} +.hist-tool-body { + padding: 10px 12px 12px; + border-radius: 0 0 var(--radius-md) var(--radius-md); + background: var(--bg); + border: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border)); + border-top: none; + display: none; +} +.hist-tool-body.open { display: block; } +.hist-tool-label { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); + margin: 8px 0 4px; +} +.hist-tool-label:first-child { margin-top: 0; } +.hist-tool-block { + font-family: var(--font-mono); + font-size: 11px; + line-height: 1.65; + color: var(--text-dim); + white-space: pre-wrap; + word-break: break-word; + overflow-x: auto; +} +.hist-tool-output { color: var(--text); } + +/* ─── Phone layout ──────────────────────────────────────────────────────── + * On phones the floating bubble stream overlaps the orb and there isn't room + * for it, so we drop it entirely and rely on the conversation panel (opened + * from the top-right chat button) as the single place to read the transcript. + * The badge still pulses there when new messages arrive while it's closed. + * We also stack the mic / stop controls above and below the orb (instead of + * left/right) so the wide live row never overflows, and let the panel take + * the full width. */ +@media (max-width: 600px) { + .bubble-stack { + display: none; + } + + .topbar { + padding: 14px 16px; + } + + .stage { + padding: 16px 12px 24px; + } + + /* Stack vertically: mic above the orb, stop below it (DOM order is + * mic → circle → stop). */ + .orb-wrap { + flex-direction: column; + gap: 14px; + } + + .circle { + /* Height-capped like the base rule so a narrow AND short box still fits. */ + width: clamp(130px, min(56vw, 52vh), 240px); + } + + /* In the column layout the side controls must collapse by HEIGHT, not + * width, so they take no vertical space until the session is live. */ + .side-btn { + width: 44px; + height: 0; + transition: opacity 0.25s ease, transform 0.25s ease, height 0.25s ease, + background 0.15s, color 0.15s, border-color 0.15s; + } + .side-btn svg { + width: 19px; + height: 19px; + } + .orb-wrap.live .side-btn { + width: 44px; + height: 44px; + } + .mic-gate-arc { width: 70px; height: 70px; } + + /* Full-screen conversation on phones — feels more deliberate than a + * narrow slide-over. It already spans top-to-bottom (inset 0); make it + * span edge-to-edge too and drop the now-pointless border/shadow. */ + .chat-panel-inner { + width: 100vw; + border-left: none; + box-shadow: none; + } +} + +/* ─── About panel ───────────────────────────────────────────────────────── + * Opened from the (i) by the wordmark. Reuses the .modal shell. + * Strictly monochrome per DESIGN.md — the only "color" is the orb, never + * here. Machine identifiers (model IDs, role tags, usernames) ride Geist + * Mono; everything human stays Inter. */ +.about-modal { + /* Roomier on tablet/desktop; the 92vw cap keeps phones full-width. */ + width: min(680px, 92vw); + max-height: 88vh; +} +.about-modal .modal-content { + max-height: 88vh; + overflow-y: auto; + gap: 20px; + padding: 26px 30px 24px; +} +.about-modal .modal-content::-webkit-scrollbar { width: 3px; } +.about-modal .modal-content::-webkit-scrollbar-track { background: transparent; } +.about-modal .modal-content::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } + +/* Links here are quiet: no permanent underline (the global dotted rule is + * too loud for a dense credit block), brighten + underline on hover only. */ +.about-modal a { + color: var(--text); + border-bottom: none; + text-decoration: none; + transition: color 0.15s ease; +} +.about-modal a:hover { + color: #fff; +} +.about-intro a:hover { + text-decoration: underline; + text-underline-offset: 2px; +} +.about-modal .ext { + width: 12px; + height: 12px; + flex: none; + opacity: 0.5; +} + +/* Title row: the demo name and the (i) read as one unit. */ +.ident-head { + display: flex; + align-items: center; + gap: 9px; +} +/* (i) trigger sits right after the title. A faint outline keeps it + * catchable without turning into a card; it fills in on hover. */ +.about-btn { + flex: none; + width: 34px; + height: 34px; + border-radius: 50%; + background: transparent; + border: 1px solid var(--border-strong); + color: var(--text-dim); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + touch-action: manipulation; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} +.about-btn:hover { + background: var(--bg-elev); + border-color: var(--text-dim); + color: var(--text); +} +.about-btn svg { + width: 20px; + height: 20px; +} + +.about-btn-mobile { + display: inline-flex; +} + +/* ── Popup intro: a plain paragraph on the project + a repo link ── */ +.about-intro p { + margin: 0; + font-size: 14px; + line-height: 1.6; + color: var(--text-dim); +} +.about-repo { + display: inline-flex; + align-items: center; + gap: 5px; + margin-top: 12px; + font-size: 14px; + font-weight: 500; +} + +/* ── Corner identity (replaces the wordmark) ── + * A compact stack in the topbar: title, one-line blurb, two meta rows. + * Monochrome; only the role/name identifiers ride the machine typeface. */ +.ident { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} +/* Reset the global underlined-anchor styling inside the identity block. */ +.ident a { + border-bottom: none; + color: inherit; + transition: color 0.15s ease; +} +.ident a:hover { + color: var(--text); + text-decoration: underline; + text-underline-offset: 2px; +} +.ident-title { + font-size: 22px; + font-weight: 600; + letter-spacing: 0.005em; + line-height: 1.1; + color: var(--text); +} +.ident-blurb { + margin: 0; + max-width: 48ch; + font-size: 14.5px; + line-height: 1.5; + font-weight: 400; + color: var(--text-dim); +} +.ident-meta { + display: flex; + flex-direction: column; + gap: 7px; + font-size: 14px; + font-weight: 400; + color: var(--text-dim); +} +.ident-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 8px; +} +.ident-label { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); +} + +/* Shared credit bits — now used in the corner identity block. */ +.sep { + color: var(--text-faint); + opacity: 0.6; +} +.hf-credit { + display: inline-flex; + align-items: center; + gap: 5px; +} +/* Brand marks keep their own color — a deliberate exception to the + * monochrome rule, for the HF and Cerebras logos only. */ +.hf-mark { + width: 14px; + height: 14px; + flex: none; + color: #ffd21e; +} +.cerebras-credit { + display: inline-flex; + align-items: center; + gap: 5px; +} +.cerebras-mark { + width: 14px; + height: 14px; + flex: none; +} +/* Usernames are identifiers, so they ride the machine typeface. */ +.handle { + font-family: var(--font-mono); + font-size: 13.5px; +} + +/* ── Pipeline (the signal flow) ── */ +.about-pipeline { + border-top: 1px solid var(--border); + padding-top: 16px; +} +.pipeline-title { + margin: 0 0 14px; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-faint); +} +.pipeline { + list-style: none; + margin: 0; + padding: 0; + position: relative; +} +/* One continuous rail threading every node, dot centers at x=8.5px. */ +.pipeline::before { + content: ""; + position: absolute; + left: 8px; + top: 7px; + bottom: 7px; + width: 1px; + background: var(--border-strong); +} +.pipeline > li { + position: relative; + padding-left: 30px; +} +/* Stages: solid node. Endpoints (you / orb): hollow node. */ +.pipe-stage::before { + content: ""; + position: absolute; + left: 5px; + top: 4px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text-dim); +} +.pipe-endpoint::before { + content: ""; + position: absolute; + left: 5px; + top: 3px; + width: 7px; + height: 7px; + border-radius: 50%; + border: 1px solid var(--text-faint); + background: var(--bg-elev); +} +.pipe-endpoint { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-faint); + padding-bottom: 12px; +} +.pipeline > li.pipe-endpoint:last-child { + padding-bottom: 0; +} +.pipe-stage { + padding-bottom: 16px; +} +.pipe-tag { + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.08em; + color: var(--text); + margin-right: 9px; +} +.pipe-job { + font-size: 14px; + color: var(--text-dim); +} +/* Middot between the job and its model link, matching the separators + * used elsewhere. */ +.pipe-job::after { + content: "·"; + margin: 0 7px; + color: var(--text-faint); +} +.pipe-note { + color: var(--text-faint); +} +/* The Cerebras link stays as quiet as the note; brightens + underlines on hover. */ +.pipe-note a { + color: inherit; +} +.pipe-note a:hover { + color: var(--text); + text-decoration: underline; + text-underline-offset: 2px; +} +.pipe-model { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 4px; + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-dim); + word-break: break-all; +} +.pipe-model:hover { + color: #fff; + text-decoration: underline; + text-underline-offset: 2px; +} +.pipe-model .ext { + width: 11px; + height: 11px; +} + +/* "Interrupted" tag on an assistant reply the user barged in on. */ +.hist-note { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); + padding: 0 2px; +} +.hist-msg.assistant .hist-note { + align-self: flex-end; +} +.hist-msg.interrupted .hist-body { + opacity: 0.7; +} + +/* Captured webcam frame shown in the transcript (camera tool result). */ +.hist-image { + display: block; + width: 100%; + max-width: 240px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + margin-top: 2px; +} + +/* ─── Tools panel ────────────────────────────────────────────────────────── + * Reuses the modal/field shell. Switches are monochrome (checked = near-white, + * the same high-contrast treatment as the primary button): color belongs to + * the voice, not the chrome. */ +.tools-intro { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: var(--text-dim); +} +.tool-list { + display: flex; + flex-direction: column; +} +.tool-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 0; +} +.tool-row-sep { + border-top: 1px solid var(--border); + margin-top: 4px; +} +.tool-info { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; +} +.tool-name { + font-size: 14px; + font-weight: 600; + color: var(--text); +} +.tool-desc { + font-size: 12.5px; + color: var(--text-dim); +} +.tool-row.disabled .tool-name, +.tool-row.disabled .tool-desc { + opacity: 0.5; +} +.tool-hint { + display: block; + font-size: 12px; + line-height: 1.4; + color: var(--text-faint); +} +.tools-key { + margin: 0 0 4px; +} + +/* Toggle switch */ +.switch { + position: relative; + display: inline-flex; + flex: none; + width: 40px; + height: 24px; + cursor: pointer; +} +.switch input { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + margin: 0; + opacity: 0; + cursor: pointer; +} +.switch-track { + position: absolute; + inset: 0; + border-radius: 999px; + background: var(--bg); + border: 1px solid var(--border-strong); + transition: background 0.15s, border-color 0.15s; +} +.switch-track::after { + content: ""; + position: absolute; + top: 50%; + left: 3px; + transform: translateY(-50%); + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--text-dim); + transition: transform 0.18s ease, background 0.15s; +} +.switch input:checked + .switch-track { + background: var(--text); + border-color: var(--text); +} +.switch input:checked + .switch-track::after { + transform: translate(16px, -50%); + background: var(--bg); +} +.switch input:focus-visible + .switch-track { + outline: 2px solid var(--text-dim); + outline-offset: 2px; +} +.switch input:disabled { + cursor: not-allowed; +} +.switch input:disabled + .switch-track { + opacity: 0.5; +} + +/* ─── Webcam preview (camera tool) ────────────────────────────────────────── + * Floating self-view, bottom-left. Mirrored for the user; the frame sent to + * the model is drawn un-mirrored (see captureSnapshot). */ +.cam-pip { + position: fixed; + left: 20px; + bottom: 20px; + width: 280px; + aspect-ratio: 4 / 3; + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--border-strong); + background: var(--bg-elev); + box-shadow: var(--shadow-soft); + z-index: 90; + opacity: 0; + transform: translateY(8px) scale(0.96); + pointer-events: none; + transition: opacity 0.22s ease, transform 0.22s ease; +} +.cam-pip.visible { + opacity: 1; + transform: none; +} +.cam-video { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transform: scaleX(-1); /* mirror the self-view only */ + background: var(--bg); +} +.cam-label { + position: absolute; + left: 8px; + bottom: 6px; + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text); + opacity: 0.8; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7); +} +.cam-flash { + position: absolute; + inset: 0; + background: #fff; + opacity: 0; + pointer-events: none; +} +.cam-pip.flash .cam-flash { + animation: cam-flash 0.4s ease; +} +@keyframes cam-flash { + 0% { opacity: 0; } + 12% { opacity: 0.85; } + 100% { opacity: 0; } +} +@media (max-width: 600px) { + /* Bottom-centred on phones. Auto margins centre it without touching the + * transform, so the slide/scale-in animation still works. */ + .cam-pip { + left: 0; + right: 0; + margin-inline: auto; + bottom: 16px; + width: min(188px, 52vw); + } + /* The credit would sit under the centred preview, so drop it while the + * camera is on. */ + body.cam-on .footer { + display: none; + } +} +@media (prefers-reduced-motion: reduce) { + .cam-pip { + transition: opacity 0.22s ease; + transform: none; + } + .cam-pip.flash .cam-flash { + animation: none; + } +} + +/* ─── Desktop type scale ──────────────────────────────────────────────────── + * A uniform step up (~+1px, title +2) for all UI text on larger screens. + * Scoped to min-width: 601px so it can't reach phones — the ≤600px layout + * keeps every size exactly as it was. */ +@media (min-width: 601px) { + .cam-label { font-size: 10.5px; } + + .bubble-role, + .hist-role { font-size: 11px; } + + .circle-caption, + .footer, + .chat-empty-title, + .hist-tool-block, + .ident-label, + .pipeline-title, + .pipe-endpoint { font-size: 12px; } + + .field > span, + .field small, + .bubble.tool, + .chat-empty-hint, + .hist-tool-name, + .pipe-tag, + .pipe-model, + .tool-hint { font-size: 13px; } + + .tool-desc { font-size: 13.5px; } + + .btn, + .bubble, + .field, + .hist-body, + .hist-tool-header, + .tools-intro { font-size: 14px; } + + .handle { font-size: 14.5px; } + + .about-intro p, + .about-repo, + .brand, + .chat-panel-header h3, + .field textarea, + .ident-meta, + .pipe-job, + .tool-name { font-size: 15px; } + + .ident-blurb { font-size: 15.5px; } + + .modal-header h2 { font-size: 17px; } + + .ident-title { font-size: 24px; } +} + +/* ─── Short viewports: fit without a vertical scrollbar ───────────────────── + * The base media queries only react to WIDTH, so a short container (e.g. the + * app embedded in a small box, or a laptop window once the browser chrome eats + * into the viewport) used to overflow vertically and get clipped. We KEEP the + * identity header, orb, caption and credits — we just undo the desktop type + * bump, tighten the vertical rhythm and let the height-capped orb shrink, so + * everything fits with comfortable margin. No opt-in needed. + * + * The single biggest space eater is the identity block (~180px at the desktop + * type scale), so this tier compacts it first. */ +@media (max-height: 780px) { + .topbar { padding: 14px 24px; } + .ident { gap: 5px; } + .ident-title { font-size: 20px; } + .ident-blurb { font-size: 13.5px; line-height: 1.4; } + .ident-meta { gap: 5px; font-size: 13px; } + .stage { padding: 12px 20px 16px; gap: 12px; } + .footer { padding: 8px 24px 10px; } + .circle { width: clamp(140px, min(38vw, 44vh), 300px); } +} + +@media (max-height: 560px) { + .topbar { padding: 8px 16px; } + .ident-title { font-size: 17px; } + /* Below this, room is tight enough that the one-line blurb and the footer + * (a verbatim duplicate of the header's "Powered by" credit) are dropped so + * the title + credits still read; the rest of the header stays. */ + .ident-blurb { display: none; } + .stage { padding: 8px 14px 10px; gap: 8px; } + .footer { display: none; } + .circle { width: clamp(120px, min(50vw, 46vh), 240px); } +} + +/* ─── Digital-human avatar as full-viewport background ────────────────── */ + +.avatar-stage { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + z-index: 0; + overflow: hidden; + background: #111; +} + +.avatar-stage iframe { + width: 100%; + height: 100%; + border: none; + display: block; + pointer-events: none; +} diff --git a/backend/vendor/s2s-cloud/demo/style.css.orig b/backend/vendor/s2s-cloud/demo/style.css.orig new file mode 100644 index 0000000..ae07a1b --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/style.css.orig @@ -0,0 +1,2646 @@ +:root { + --bg: #0a0b10; + --bg-elev: #13151c; + --bg-elev-2: #1b1e29; + --border: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.16); + --text: #f5f6fa; + --text-dim: rgba(245, 246, 250, 0.65); + --text-faint: rgba(245, 246, 250, 0.42); + + --accent: #8b7dff; + --accent-2: #22d3ee; + --listening: #22d3ee; + --speaking: #8b7dff; + --processing: #f59e0b; + --error: #ff6a75; + --success: #34d399; + + /* Mono is the "machine voice": reserved for system text the app emits — + * the orb's status caption, tool calls, the transport tag. Body/UI stays + * Inter. */ + --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, monospace; + + /* Thesis: color belongs to the voice. The chrome is monochrome; saturated + * hue appears only on the orb and on these tiny role echoes, which mirror + * the orb's own state colors (you listening = cyan, assistant = violet, + * tool = amber) so the transcript reads in the same color language. */ + --voice-user: var(--accent-2); + --voice-assistant: var(--accent); + --voice-tool: var(--processing); + + /* Smoothed mic RMS in [0..1], updated every frame from JS while a session + * is active. Used by audio-reactive circle states. */ + --audio-level: 0; + /* Five log-spaced frequency bands extracted from the mic analyser; + * drive each bar's height independently for a real "spectrum" feel. */ + --bar0: 0; + --bar1: 0; + --bar2: 0; + --bar3: 0; + --bar4: 0; + + --radius-sm: 8px; + --radius-md: 14px; + --radius-lg: 22px; + + --shadow-soft: 0 10px 40px rgba(0, 0, 0, 0.35); + + color-scheme: dark; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + height: 100%; +} + +body { + font-family: "Inter", system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background: radial-gradient(ellipse at 50% 20%, #1a1c28 0%, var(--bg) 60%); + color: var(--text); + min-height: 100vh; + overflow-x: hidden; + -webkit-font-smoothing: antialiased; +} + +button { + font-family: inherit; +} + +a { + color: var(--text); + text-decoration: none; + border-bottom: 1px solid var(--border-strong); +} +a:hover { + border-bottom-color: var(--text); +} + +#app { + display: grid; + grid-template-rows: auto 1fr auto; + min-height: 100vh; +} + +.hidden { + display: none !important; +} + +/* ─── Topbar ──────────────────────────────────────────────────────────── */ + +/* Topbar is just a floating row of controls over the stage - no + * separator line, no background. Same story for the footer. Keeps the + * app feeling like one continuous canvas. */ +.topbar { + display: flex; + /* Top-align so the right control cluster sits up with the title instead of + * floating to the vertical middle of the tall identity block. */ + align-items: flex-start; + justify-content: space-between; + padding: 18px 28px; + /* Default stacking (below the conversation panel, z 200) so the panel drawer + * covers the controls when it's open. */ +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + font-weight: 600; + font-size: 14px; + letter-spacing: 0.01em; + color: var(--text-dim); +} + +/* Transport tag rides the wordmark as a system label, not prose — mono, + * dimmed, the only mono in the topbar. */ +.brand-tag { + font-family: var(--font-mono); + font-weight: 500; + font-size: 11px; + letter-spacing: 0.02em; + opacity: 0.55; +} + +.brand .brand-logo { + width: 26px; + height: 26px; + object-fit: contain; + flex: none; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.45)); + transition: transform 0.2s ease; +} +.brand .brand-logo:hover { + transform: translateY(-1px) rotate(-2deg); +} + +/* Narrow viewports (typically the mobile shell iframe and phone-sized + browser windows): the full product name pushes the topbar's right + cluster off-screen. Keep the logo as the brand cue and drop the + wordmark - the orb already gives enough context. */ +@media (max-width: 600px) { + /* The full identity stack is too tall for a phone topbar. Show only the + * title + (i); the popup carries the blurb, credits and pipeline. + * `.brand` prefix raises specificity so this beats the later base + * `.ident-meta { display: flex }` rule regardless of source order. */ + .brand .ident-blurb, + .brand .ident-meta { + display: none; + } + .ident-title { + font-size: 18px; + } + /* (i) moves into the right-hand control cluster on phones, leaving the + * title alone on the left. */ + .brand .about-btn { + display: none; + } + .about-btn-mobile { + display: inline-flex; + } + /* Keep phone icons at the current compact size — the desktop bump above + * shouldn't leak into the mobile topbar. */ + .topbar-right .icon-btn { + width: 36px; + height: 36px; + } + .topbar-right .icon-btn svg { + width: 18px; + height: 18px; + } + /* Keep the account control compact on phones: avatar-only chip, shorter pill. */ + .account-chip { + height: 36px; + padding: 0 6px; + } + .account-handle { + display: none; + } + .signin-pill { + height: 36px; + padding: 0 12px; + } +} + +.topbar-right { + display: flex; + align-items: center; + gap: 10px; +} + +/* The HF user pill: compact avatar + handle. Rendered as a flex row so + * the avatar stays aligned with the text baseline even when the text + * wraps on narrow viewports. */ +.hf-user { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text-dim); + padding: 4px 10px 4px 4px; + border-radius: 999px; + background: var(--bg-elev); + border: 1px solid var(--border); + line-height: 1; +} + +.hf-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + object-fit: cover; + background: color-mix(in srgb, var(--accent) 25%, var(--bg-elev-2)); + /* Hidden until an actual URL loads (see `setHfAvatar` in main.ts). + * Keeps the initial login flash from showing a broken image icon. */ + opacity: 0; + transition: opacity 0.25s ease; + flex: none; +} +.hf-avatar.loaded { + opacity: 1; +} + +.hf-user-name { + white-space: nowrap; +} + +/* ─── Transport pill ────────────────────────────────────────────── + * Surface the actual network path WebRTC picked for the robot peer + * connection (LAN, direct-through-NAT, or TURN-relayed). Lets the + * user spot at a glance when audio is going through the internet + * instead of staying on-prem. + */ +.transport-pill { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.02em; + padding: 5px 10px; + border-radius: 999px; + background: var(--bg-elev); + border: 1px solid var(--border); + color: var(--text-dim); + user-select: none; + transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease; +} + +.transport-pill .transport-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: currentColor; + box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 25%, transparent); + flex: none; +} + +/* Bitrate readout sits after the kind label. Dimmer + monospace so the + * kind (LAN / Direct / Relayed) stays the primary info and the numbers + * don't wiggle the layout as digits change. */ +.transport-pill .transport-bitrate { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + color: color-mix(in srgb, currentColor 75%, var(--text-dim)); + opacity: 0.85; + padding-left: 6px; + border-left: 1px solid color-mix(in srgb, currentColor 25%, transparent); +} +.transport-pill .transport-bitrate:empty { + display: none; +} + +.transport-pill.transport-checking { + color: var(--text-dim); +} +.transport-pill.transport-checking .transport-dot { + animation: transport-blink 1.2s ease-in-out infinite; +} + +.transport-pill.transport-lan { + color: #4ade80; + border-color: color-mix(in srgb, #4ade80 35%, var(--border)); + background: color-mix(in srgb, #4ade80 10%, var(--bg-elev)); +} + +.transport-pill.transport-direct { + color: #60a5fa; + border-color: color-mix(in srgb, #60a5fa 35%, var(--border)); + background: color-mix(in srgb, #60a5fa 10%, var(--bg-elev)); +} + +.transport-pill.transport-relay { + color: #fbbf24; + border-color: color-mix(in srgb, #fbbf24 35%, var(--border)); + background: color-mix(in srgb, #fbbf24 10%, var(--bg-elev)); +} + +@keyframes transport-blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} + +.icon-btn { + background: var(--bg-elev); + border: 1px solid var(--border); + color: var(--text-dim); + width: 36px; + height: 36px; + border-radius: var(--radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + touch-action: manipulation; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} +.icon-btn:hover { + background: var(--bg-elev-2); + color: var(--text); + border-color: var(--border-strong); +} +/* Topbar control cluster reads a touch bigger on desktop. Phones keep the + * compact 36px (reset in the mobile media query). */ +.topbar-right .icon-btn { + width: 40px; + height: 40px; +} +.topbar-right .icon-btn svg { + width: 20px; + height: 20px; +} + +/* ─── Account (HF login chip + sign-in pill + popover) ─────────────────────── */ +.account { + position: relative; + display: inline-flex; + align-items: center; +} +/* Signed-out: a compact pill that reads as the primary affordance. */ +.signin-pill { + display: inline-flex; + align-items: center; + gap: 7px; + height: 40px; + padding: 0 14px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-strong); + background: var(--bg-elev); + color: var(--text); + font-size: 13px; + font-weight: 600; + text-decoration: none; + transition: background 0.15s, border-color 0.15s; +} +.signin-pill svg { + width: 16px; + height: 16px; + flex: none; +} +.signin-pill:hover { + background: var(--bg-elev-2); + border-color: var(--text); +} +/* Narrow viewports: collapse the pill to an icon-only square. */ +@media (max-width: 800px) { + .signin-pill { + gap: 0; + width: 40px; + padding: 0; + justify-content: center; + } + .signin-pill span { + display: none; + } +} +/* Signed-in: avatar + handle chip. */ +.account-chip { + display: inline-flex; + align-items: center; + gap: 8px; + height: 40px; + padding: 0 10px 0 6px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: var(--bg-elev); + color: var(--text-dim); + cursor: pointer; + transition: background 0.15s, color 0.15s, border-color 0.15s; +} +.account-chip:hover { + background: var(--bg-elev-2); + color: var(--text); + border-color: var(--border-strong); +} +.account-avatar { + width: 26px; + height: 26px; + border-radius: 50%; + object-fit: cover; + flex: none; +} +.account-avatar-fallback { + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--bg-elev-2); + color: var(--text); + font-size: 12px; + font-weight: 700; +} +.account-handle { + font-size: 13px; + font-weight: 600; + max-width: 12ch; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.account-pro { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + padding: 2px 5px; + border-radius: 5px; + background: var(--text); + color: var(--bg); +} +/* Org "Team" badge: same pill as PRO, accent-coloured so it reads as + * unlimited without claiming a paid PRO subscription. */ +.account-team { + background: var(--accent); + color: var(--bg); +} +.account-pop { + position: absolute; + top: calc(100% + 8px); + right: 0; + min-width: 200px; + padding: 6px; + border-radius: var(--radius-md); + border: 1px solid var(--border-strong); + background: var(--bg-elev); + box-shadow: var(--shadow-soft); + z-index: 50; +} +.account-pop[hidden] { + display: none; +} +.account-pop-row { + padding: 8px 10px; +} +.account-pop-name { + font-weight: 600; + font-size: 13px; +} +.account-pop-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding-top: 0; + font-size: 12px; + color: var(--text-dim); +} +.account-tier { + font-weight: 600; + color: var(--text); +} +.account-pop-link { + display: block; + padding: 8px 10px; + border-radius: var(--radius-sm); + color: var(--text-dim); + font-size: 13px; + text-decoration: none; + transition: background 0.15s, color 0.15s; +} +.account-pop-link:hover { + background: var(--bg-elev-2); + color: var(--text); +} +.account-signout { + border-top: 1px solid var(--border); + margin-top: 4px; + padding-top: 10px; + border-radius: 0 0 var(--radius-sm) var(--radius-sm); +} + +/* ─── Daily-limit modal ────────────────────────────────────────────────────── */ +.limit-modal { + width: min(400px, 92vw); +} +.limit-card { + position: relative; + align-items: center; + text-align: center; + gap: 14px; + padding: 34px 28px 26px; +} +.limit-close { + position: absolute; + top: 12px; + right: 12px; +} +/* HF smiling face (brand yellow) on a neutral badge — friendly, not a stop sign. + * The logo is the single pop of colour, so the badge itself stays quiet. */ +.limit-badge { + width: 66px; + height: 66px; + border-radius: 50%; + display: grid; + place-items: center; + margin: 2px auto 2px; + background: var(--bg-elev-2); + border: 1px solid var(--border-strong); + box-shadow: 0 0 0 6px rgba(255, 210, 30, 0.06); +} +.limit-badge .hf-logo { + width: 46px; + height: auto; +} +.limit-title { + margin: 0; + font-size: 20px; + font-weight: 700; + letter-spacing: 0; +} +.limit-msg { + color: var(--text-dim); + font-size: 14px; + line-height: 1.55; + margin: 0; + max-width: 30ch; +} +.limit-cta { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + margin-top: 4px; +} +.limit-cta .hf-logo { + width: 20px; + height: auto; + flex: none; +} +.limit-note { + margin: 0; + font-size: 12px; + color: var(--text-faint); +} +#limit-cta[hidden] { + display: none; +} + +/* ─── Stage ───────────────────────────────────────────────────────────── */ + +.stage { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px 24px 32px; + gap: 20px; + /* Let the stage shrink inside the grid's 1fr row so a height-capped orb can + * give back space rather than forcing the whole app to overflow. */ + min-height: 0; +} + +/* ─── Central circle ──────────────────────────────────────────────────── */ + +/* Wraps the orb and its two side controls so they stay aligned on one row. */ +.orb-wrap { + position: relative; + display: flex; + align-items: center; + justify-content: center; + gap: clamp(18px, 3vw, 32px); +} + +.circle { + position: relative; + /* Cap by height as well as width (min(vw, vh)-style): in a short container + * the orb shrinks instead of overflowing, so the page stays scroll-free even + * when embedded in a small box. On normal screens the 320px cap still wins, + * so the desktop size is unchanged. */ + width: clamp(140px, min(38vw, 46vh), 320px); + aspect-ratio: 1 / 1; + border-radius: 50%; + border: none; + background: transparent; + padding: 0; + cursor: pointer; + outline: none; + display: grid; + place-items: center; + color: var(--glow, var(--accent)); + transition: transform 0.18s ease, filter 0.18s ease; + -webkit-tap-highlight-color: transparent; + /* Treat taps as immediate clicks (no 300ms delay / double-tap-zoom). */ + touch-action: manipulation; +} +.circle:hover { + filter: brightness(1.08); +} +.circle:active { + transform: scale(0.97); + filter: brightness(0.92); +} +.circle:focus-visible .circle-core { + outline: 2px solid var(--glow, var(--accent)); + outline-offset: 6px; +} +.circle[disabled] { + cursor: default; + opacity: 0.75; +} + +.circle-glow { + position: absolute; + inset: 0; + border-radius: 50%; + background: radial-gradient(circle at center, var(--glow, var(--accent)) 0%, transparent 65%); + filter: blur(28px); + opacity: 0.5; + transform: scale(1); + transition: opacity 0.25s, background 0.25s, transform 1.4s ease-in-out; + pointer-events: none; +} + +/* Two nested circular rings: the inner tracks the core edge, the outer + * expands / fades to convey "audio radiating out" during speaking. */ +.circle-ring, +.circle-ring-outer { + position: absolute; + top: 50%; + left: 50%; + border-radius: 50%; + transform: translate(-50%, -50%); + pointer-events: none; + transition: opacity 0.4s ease, border-color 0.4s ease, transform 0.3s ease; +} +.circle-ring { + width: 82%; + height: 82%; + border: 1.5px solid color-mix(in srgb, var(--glow, var(--accent)) 35%, transparent); + opacity: 0.35; +} +.circle-ring-outer { + width: 94%; + height: 94%; + border: 1px solid color-mix(in srgb, var(--glow, var(--accent)) 22%, transparent); + opacity: 0; +} + +.circle-core { + position: relative; + width: 72%; + height: 72%; + border-radius: 50%; + background: radial-gradient( + circle at 35% 28%, + color-mix(in srgb, var(--glow, var(--accent)) 28%, transparent), + color-mix(in srgb, var(--glow, var(--accent)) 10%, transparent) 55%, + color-mix(in srgb, var(--glow, var(--accent)) 5%, transparent) + ); + border: 2px solid color-mix(in srgb, var(--glow, var(--accent)) 40%, transparent); + display: grid; + place-items: center; + box-shadow: + 0 0 32px color-mix(in srgb, var(--glow, var(--accent)) 22%, transparent), + inset 0 0 28px color-mix(in srgb, var(--glow, var(--accent)) 18%, transparent); + transition: background 0.4s ease, border-color 0.4s ease, box-shadow 0.4s ease, transform 0.4s ease; +} + +/* Indicator slot: a single SVG / spinner / bar group is visible at a time, + * driven by the state class on `.circle`. */ +.circle-indicator { + position: relative; + width: 44%; + height: 44%; + display: grid; + place-items: center; + color: var(--glow, var(--accent)); +} +.circle-indicator > .ind { + grid-area: 1 / 1; + opacity: 0; + transform: scale(0.85); + transition: opacity 0.25s ease, transform 0.25s ease; + pointer-events: none; +} +.circle-indicator > svg.ind { + width: 60%; + height: 60%; +} + +/* Spinner: a rotating ring gap, CSS-only. + * + * Note: the base `.circle-indicator > .ind` rule forces `transform: + * scale(.85)` / `scale(1)` on every indicator to drive the show/hide + * transition. If we only rotate here, the browser has to interpolate + * between `scale(1)` and `rotate(360deg)` (two different transform + * functions), which produces a broken, barely-moving animation. So we + * include the scale explicitly in the keyframes and bump specificity + * with `!important` so the spinner always wins over the state rule. */ +.ind-spinner { + width: 48%; + height: 48%; + border: 3px solid currentColor; + border-right-color: transparent; + border-radius: 50%; + opacity: 0; + animation: ind-spin 0.9s linear infinite; +} + +/* Thinking dots: 3 soft pulsing dots while the model is composing a + * response. Apple-style cadence: each dot scales up and brightens in + * turn, staggered by ~160 ms. Per-dot animation is on the child, so + * the parent's scale(.85 → 1) show/hide transform composes cleanly. */ +.ind-thinking { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + width: 60%; + height: 60%; +} +.ind-thinking .dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: currentColor; + opacity: 0.3; + animation: thinking-dot 1.25s ease-in-out infinite; +} +.ind-thinking .dot:nth-child(1) { animation-delay: 0s; } +.ind-thinking .dot:nth-child(2) { animation-delay: 0.16s; } +.ind-thinking .dot:nth-child(3) { animation-delay: 0.32s; } + +/* Bars: 5 vertical pills driven by --bar0..--bar4 CSS vars. */ +.ind-bars { + display: flex; + align-items: center; + gap: 5px; + height: 42%; +} +.ind-bars .bar { + width: 4px; + min-height: 4px; + border-radius: 3px; + background: currentColor; + opacity: 0.7; + --h: var(--bar0, 0); + height: calc(4px + var(--h) * 36px); + transition: height 0.08s ease-out, opacity 0.08s ease-out; +} +.ind-bars .bar:nth-child(1) { --h: var(--bar0); } +.ind-bars .bar:nth-child(2) { --h: var(--bar1); } +.ind-bars .bar:nth-child(3) { --h: var(--bar2); } +.ind-bars .bar:nth-child(4) { --h: var(--bar3); } +.ind-bars .bar:nth-child(5) { --h: var(--bar4); } +.ind-bars .bar { + opacity: calc(0.55 + 0.45 * var(--h)); +} + +/* Active indicator per state. + * + * Note: `ai-speaking` deliberately has NO indicator here. The orb + * itself becomes the indicator by pulsing on Reachy's voice (see the + * `--ai-audio-level` rules further down), which is a lot clearer and + * less confusing than reusing the mic-bars (which viewers would read + * as "you are speaking"). */ +.state-signed-out .ind-connect, +.state-authenticated .ind-mic, +.state-ready .ind-mic, +.state-connecting .ind-spinner, +.state-connected .ind-spinner, +.state-auto-selecting .ind-spinner, +.state-starting .ind-spinner, +.state-queued .ind-spinner, +.state-processing .ind-thinking, +.state-listening .ind-bars, +.state-user-speaking .ind-bars, +.state-ai-speaking .ind-voice, +.state-error .ind-error { + opacity: 1; + transform: scale(1); +} + +/* AI speaking indicator: speaker + two sound waves. + * + * Each wave pulses outward (opacity + stroke grow) with a quarter- + * beat offset so it reads as sound radiating out. Kept as a pure + * CSS animation so the icon is always visually alive even between + * syllables when --ai-audio-level momentarily dips. */ +.ind-voice .wave { + transform-origin: 50% 50%; + animation: voice-wave 1.35s ease-out infinite; +} +.ind-voice .wave-1 { animation-delay: 0s; } +.ind-voice .wave-2 { animation-delay: 0.35s; } + +/* Idle indicators: a chain-link "connect" glyph for the signed-out + * step (invites the user to authenticate with HF) and a microphone + * once a session is ready. Both picked up by the generic + * `.circle-indicator > svg.ind` sizing rule (60% × 60% of the slot) + * and rely on per-state opacity transitions for show / hide. */ +.ind-connect, +.ind-mic { + color: color-mix(in srgb, var(--glow, var(--accent)) 85%, white); +} + +/* ─── Caption below the circle ────────────────────────────────────────── */ + +/* The caption under the orb is meant to whisper, not shout: micro-label + * vibe, uppercase, letter-spaced, muted. Only appears for actionable / + * transitional states (see STATE_VIEWS). During a live conversation the + * orb alone carries the state so we collapse this row entirely. */ +.circle-caption { + margin: 0; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-faint); + min-height: 1.2em; + text-align: center; + opacity: 0.75; + transition: opacity 0.25s ease, color 0.25s ease, transform 0.25s ease, + min-height 0.25s ease; +} +.circle-caption.empty { + opacity: 0; + min-height: 0; + transform: translateY(-4px); + pointer-events: none; +} +.circle-caption.muted { + color: var(--text-faint); + opacity: 0.65; +} +.circle-caption.error { + color: var(--error); + opacity: 1; + letter-spacing: 0.08em; +} + +/* Warm, human line under the mono caption — sentence case, only while queued. + * The caption keeps the terse position; this reassures. */ +.circle-subcaption { + margin: 8px 0 0; + max-width: 32ch; + font-size: 13.5px; + line-height: 1.5; + color: var(--text-dim); + text-align: center; + text-wrap: balance; + transition: opacity 0.25s ease; +} +.circle-subcaption[hidden] { display: none; } + +/* Queue actions sit under the caption: "Join now" (primary, only when it's your + * turn) stacked above the quiet "Leave queue" escape hatch. */ +.queue-actions { + margin-top: 16px; + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} +.queue-actions[hidden] { display: none; } + +/* "Join now": the one call to action in the queue flow, so it reads as inviting + * (filled accent) while everything around it stays quiet. */ +.join-queue-btn { + padding: 10px 26px; + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: #0b0b10; + background: var(--accent); + border: none; + border-radius: 999px; + cursor: pointer; + font-variant-numeric: tabular-nums; + transition: transform 0.12s ease, filter 0.2s ease; +} +.join-queue-btn:hover { filter: brightness(1.08); } +.join-queue-btn:active { transform: scale(0.97); } +.join-queue-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 3px; +} +.join-queue-btn[hidden] { display: none; } + +/* "Leave queue": a quiet outlined pill, understated so it reads as an escape + * hatch, not a CTA. */ +.leave-queue-btn { + padding: 7px 16px; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-faint); + background: transparent; + border: 1px solid color-mix(in srgb, var(--text-faint) 35%, transparent); + border-radius: 999px; + cursor: pointer; + transition: color 0.2s ease, border-color 0.2s ease, background 0.2s ease; +} +.leave-queue-btn:hover { + color: var(--text); + border-color: color-mix(in srgb, var(--text-faint) 60%, transparent); + background: color-mix(in srgb, var(--text-faint) 8%, transparent); +} +.leave-queue-btn:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} +.leave-queue-btn[hidden] { display: none; } + +/* ─── Tool-call toaster ───────────────────────────────────────────────── */ + +/* A small, non-interactive pill that appears below the circle when the + * model invokes a tool (move_head, play_move). Sits just under the + * state caption, collapses to zero height when no toast is active. */ +.tool-toast { + display: inline-flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding: 6px 12px; + border-radius: 999px; + border: 1px solid var(--border-strong); + background: color-mix(in srgb, var(--bg-elev-2) 78%, transparent); + color: var(--text-dim); + font-size: 12px; + font-weight: 500; + letter-spacing: 0.01em; + line-height: 1; + white-space: nowrap; + max-width: 80vw; + overflow: hidden; + text-overflow: ellipsis; + + opacity: 0; + transform: translateY(-4px) scale(0.96); + pointer-events: none; + transition: opacity 0.22s ease, transform 0.22s ease; +} +.tool-toast.visible { + opacity: 1; + transform: translateY(0) scale(1); +} +.tool-toast-icon { + width: 14px; + height: 14px; + flex: none; + color: color-mix(in srgb, var(--voice-tool) 80%, white); + animation: tool-toast-spin 3.2s linear infinite; + animation-play-state: paused; +} +.tool-toast.visible .tool-toast-icon { + animation-play-state: running; +} +.tool-toast-text { + display: inline-block; + overflow: hidden; + text-overflow: ellipsis; +} +@keyframes tool-toast-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +/* ─── Side controls (mic / stop) ──────────────────────────────────────── */ + +/* Mic button + its radial noise-gate arc. The wrapper centres the button; the + arc SVG is an absolute overlay larger than the button, revealed with the + live session (it has no layout footprint, so the idle collapse is unaffected). */ +.mic-gate { + position: relative; + flex: none; + display: inline-flex; + align-items: center; + justify-content: center; +} +.mic-gate-arc { + position: absolute; + left: 50%; + top: 50%; + width: 80px; + height: 80px; + transform: translate(-50%, -50%); + pointer-events: none; /* only the hit-path below catches drags */ + opacity: 0; + transition: opacity 0.25s ease; +} +.orb-wrap.live .mic-gate-arc { opacity: 1; } +.mga-track { + stroke: rgba(255, 255, 255, 0.1); /* hairline; recedes until needed */ + stroke-width: 1.5; + stroke-linecap: round; +} +.mga-fill { + stroke: var(--accent-2); + stroke-width: 2; + stroke-linecap: round; + opacity: 0.85; + transition: stroke-dashoffset 0.06s linear; +} +/* Threshold setpoint: a bead riding the ring. White with a thin dark outline so + it stays legible over the moving fill; it recolors to cyan with a soft glow + the moment the live level crosses it (the gate opening). */ +.mga-handle { + fill: var(--text); + stroke: none; + transition: fill 0.2s ease, filter 0.2s ease; +} +.mic-gate.gate-open .mga-handle { + fill: var(--accent-2); + filter: drop-shadow(0 0 3px var(--accent-2)); +} +.mga-hit { + stroke: transparent; + stroke-width: 16; + pointer-events: none; /* enabled only while live (below) */ + cursor: pointer; + touch-action: none; +} +/* Only catch drags during a live call; when idle the arc is hidden and must + not steal clicks near the collapsed mic button / orb. */ +.orb-wrap.live .mga-hit { pointer-events: stroke; } + +.side-btn { + flex: none; + width: 52px; + height: 52px; + border-radius: 50%; + border: 1px solid var(--border-strong); + /* Brighter background so the buttons actually stand out against the + * deep-blue stage gradient; previous var(--bg-elev) was too close to + * the page bg to be readable. */ + background: color-mix(in srgb, var(--bg-elev-2) 80%, #2a2e3c); + color: var(--text); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + touch-action: manipulation; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.35), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + /* Hidden by default: take up no space until the session is live. The + * `width: 0` collapse keeps the orb centered on the idle screen. */ + opacity: 0; + transform: scale(0.55); + width: 0; + padding: 0; + pointer-events: none; + overflow: hidden; + transition: opacity 0.25s ease, transform 0.25s ease, width 0.25s ease, + background 0.15s, color 0.15s, border-color 0.15s; +} +.side-btn:hover { + background: color-mix(in srgb, var(--bg-elev-2) 60%, #323746); + border-color: var(--text-dim); +} +.side-btn svg { + width: 22px; + height: 22px; + flex: none; +} +.side-btn .mic-off { display: none; } +.side-btn.muted { + color: #fff; + border-color: var(--error); + background: color-mix(in srgb, var(--error) 70%, #1a0e13); +} +.side-btn.muted .mic-on { display: none; } +.side-btn.muted .mic-off { display: block; } + +/* Stop button: subtle warm tint so "end" reads as destructive. */ +#stop-btn:hover { + color: #fff; + border-color: color-mix(in srgb, var(--error) 60%, var(--border-strong)); + background: color-mix(in srgb, var(--error) 25%, var(--bg-elev-2)); +} + +/* Reveal when the session is live: buttons flank the orb on a flex row. */ +.orb-wrap.live .side-btn { + opacity: 1; + transform: scale(1); + width: 52px; + pointer-events: auto; +} + +/* Disable every transition / animation during the first paint so the + * orb doesn't fade-and-scale in when the page loads. `main.ts` removes + * the class after one animation frame. */ +body.booting, +body.booting *, +body.booting *::before, +body.booting *::after { + transition: none !important; + animation-duration: 0s !important; + animation-delay: 0s !important; +} + +/* ─── Circle animation keyframes ──────────────────────────────────────── */ + +/* Slow, subtle breathing for "warm idle" states. */ +@keyframes breathe { + 0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.4; } + 50% { transform: translate(-50%, -50%) scale(1.06); opacity: 0.15; } +} + +/* Outer ring expanding and fading - conveys "I am producing audio". */ +@keyframes ring-out { + 0% { transform: translate(-50%, -50%) scale(1); opacity: 0.35; } + 100% { transform: translate(-50%, -50%) scale(1.18); opacity: 0; } +} + +/* Soft inner scale for the core while talking. */ +@keyframes core-breathe { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.04); } +} + +/* Subtle glow throb used for "thinking" — dimmer than speaking. */ +@keyframes thinking { + 0%, 100% { transform: scale(1); opacity: 0.7; } + 50% { transform: scale(0.96); opacity: 0.45; } +} + +/* Individual dot pulse for the 3-dot processing indicator. */ +@keyframes thinking-dot { + 0%, 60%, 100% { transform: scale(0.7); opacity: 0.3; } + 30% { transform: scale(1.15); opacity: 1; } +} + +/* Sound-wave pulse: arc fades in, scales up slightly, fades out. + * The transform-origin is the speaker's center (roughly x=12), so + * the waves feel like they're emanating from the cone. */ +@keyframes voice-wave { + 0% { opacity: 0; transform: scale(0.7); } + 30% { opacity: 1; transform: scale(1); } + 70% { opacity: 0.2; transform: scale(1.12); } + 100% { opacity: 0; transform: scale(0.7); } +} + +@keyframes ind-spin { + /* Scale kept at 1 so we don't fight with the indicator's base + * show/hide transform (see `.ind-spinner` for the rationale). */ + from { transform: scale(1) rotate(0deg); } + to { transform: scale(1) rotate(360deg); } +} + +/* ─── State-specific colors ──────────────────────────────────────────── */ + +.circle.state-signed-out { --glow: #8b7dff; } +.circle.state-authenticated, +.circle.state-ready { --glow: #34d399; } +.circle.state-connecting, +.circle.state-connected, +.circle.state-auto-selecting, +.circle.state-starting { --glow: #facc15; } +/* Queued: a calm slate glow, distinct from connecting's active yellow — this is + * waiting, not working. The spinner turns slowly and the ring breathes. */ +.circle.state-queued { --glow: #94a3b8; } +.circle.state-queued .ind-spinner { animation-duration: 2.4s; } +.circle.state-queued .circle-ring { animation: breathe 2.6s ease-in-out infinite; } +/* Your turn: a slot is held for you — the orb warms to the accent and breathes a + * little quicker, an invitation to join. */ +.circle.state-your-turn { --glow: var(--accent); } +.circle.state-your-turn .circle-ring { animation: breathe 1.6s ease-in-out infinite; } +.circle.state-listening, +.circle.state-user-speaking { --glow: var(--listening); } +.circle.state-processing { --glow: var(--processing); } +.circle.state-ai-speaking { --glow: var(--speaking); } +.circle.state-error { --glow: var(--error); } + +/* Idle / ready: gentle breathing of the inner ring. Kept out of the + * `signed-out` state so the very first paint on page load stays quiet + * (the orb now shows the Reachy head silhouette, no need to also pulse). */ +.circle.state-authenticated .circle-ring, +.circle.state-ready .circle-ring { + animation: breathe 2.4s ease-in-out infinite; +} + +/* Connecting flows: subtle glow throb so the orb feels thoughtful + * while the session is being negotiated. Kept off `processing` on + * purpose - the 3 thinking dots already pulse, and layering another + * throb on top competes with them for attention. */ +.circle.state-connecting .circle-core, +.circle.state-connected .circle-core, +.circle.state-auto-selecting .circle-core, +.circle.state-starting .circle-core { + animation: thinking 1.4s ease-in-out infinite; +} + +/* User is speaking: the mic RMS drives scale + opacity via --audio-level. */ +.circle.state-user-speaking .circle-ring { + animation: none; + opacity: calc(0.25 + 0.55 * var(--audio-level)); + transform: translate(-50%, -50%) scale(calc(1 + 0.08 * var(--audio-level))); + transition: transform 0.08s linear, opacity 0.08s linear; +} +.circle.state-user-speaking .circle-ring-outer { + opacity: calc(0.1 + 0.35 * var(--audio-level)); + transform: translate(-50%, -50%) scale(calc(1 + 0.12 * var(--audio-level))); + transition: transform 0.08s linear, opacity 0.08s linear; +} +.circle.state-listening .circle-ring { + animation: breathe 2s ease-in-out infinite; +} + +/* Assistant is speaking: the whole orb breathes in sync with Reachy's + * voice instead of running a fixed timer. `--ai-audio-level` (0-1) is + * updated at display rate by AiLevelMonitor from the OpenAI output + * track, so every syllable visibly moves the core + outer ring. This + * reads instantly as "the orb is the voice" and completely avoids the + * ambiguity of bars-vs-mic the user flagged. */ +.circle.state-ai-speaking .circle-core { + animation: none; + transform: scale(calc(1 + 0.09 * var(--ai-audio-level, 0))); + transition: transform 0.08s linear; +} +.circle.state-ai-speaking .circle-ring { + animation: none; + opacity: calc(0.3 + 0.5 * var(--ai-audio-level, 0)); + transform: translate(-50%, -50%) scale(calc(1 + 0.05 * var(--ai-audio-level, 0))); + transition: transform 0.08s linear, opacity 0.08s linear; +} +.circle.state-ai-speaking .circle-ring-outer { + animation: none; + opacity: calc(0.15 + 0.55 * var(--ai-audio-level, 0)); + transform: translate(-50%, -50%) scale(calc(1 + 0.18 * var(--ai-audio-level, 0))); + transition: transform 0.08s linear, opacity 0.08s linear; +} +.circle.state-ai-speaking .circle-glow { + opacity: calc(0.35 + 0.45 * var(--ai-audio-level, 0)); + transition: opacity 0.08s linear; +} + +/* ─── Robot picker ────────────────────────────────────────────────────── */ + +.robot-picker { + width: min(420px, 92vw); + background: var(--bg-elev); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 14px 16px; + box-shadow: var(--shadow-soft); +} + +.picker-title { + margin: 0 0 10px; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); +} + +.robot-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.robot-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 14px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-elev-2); + cursor: pointer; + transition: border-color 0.15s, background 0.15s; +} +.robot-card:hover { + border-color: var(--border-strong); +} +.robot-card.selected { + border-color: var(--border-strong); + background: var(--bg-elev); +} + +.robot-card .name { + font-weight: 600; + font-size: 14px; +} +.robot-card .id { + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-faint); +} + +.robot-empty { + padding: 14px; + text-align: center; + color: var(--text-faint); + font-size: 13px; +} + +/* ─── Footer ──────────────────────────────────────────────────────────── */ + +.footer { + padding: 12px 28px 18px; + font-size: 11px; + letter-spacing: 0.02em; + color: var(--text-faint); + display: flex; + justify-content: center; + opacity: 0.55; + transition: opacity 0.25s ease; +} +.footer:hover { + opacity: 0.9; +} +.footer a { + color: inherit; + text-decoration: none; + border-bottom: 1px dotted currentColor; +} +.footer a:hover { + color: var(--text-dim); +} +/* While the webcam preview sits bottom-left, push the credit to the + * bottom-right so the two don't collide. */ +body.cam-on .footer { + justify-content: flex-end; +} + +/* ─── Modal ───────────────────────────────────────────────────────────── */ + +.modal { + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 0; + background: var(--bg-elev); + color: var(--text); + width: min(440px, 92vw); + box-shadow: var(--shadow-soft); +} +.modal::backdrop { + background: rgba(8, 9, 13, 0.65); + backdrop-filter: blur(4px); +} + +.modal-content { + display: flex; + flex-direction: column; + gap: 16px; + padding: 22px 24px 20px; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 4px; +} +.modal-header h2 { + margin: 0; + font-size: 16px; + font-weight: 600; + letter-spacing: 0.02em; +} + +.field[hidden] { display: none; } +.field { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + font-weight: 500; + color: var(--text-dim); +} +.field > span { + color: var(--text); + font-weight: 600; + font-size: 12px; + letter-spacing: 0.04em; + text-transform: uppercase; +} +.field input, +.field select, +.field textarea { + font-family: inherit; + font-size: 14px; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 10px 12px; + outline: none; + transition: border-color 0.15s; + resize: vertical; +} +.field textarea { + min-height: 84px; +} +.field input:focus, +.field select:focus, +.field textarea:focus { + border-color: var(--text-dim); +} +/* Deploy-pinned server URL: shown for transparency, not editable. */ +.field input[readonly] { + color: var(--text-dim); + border-style: dashed; + cursor: default; +} +.field input[readonly]:focus { + border-color: var(--border); +} +.field small { + color: var(--text-faint); + font-size: 12px; + line-height: 1.4; +} +.field small.error { color: var(--error); } +.field small code { + background: var(--bg); + padding: 1px 5px; + border-radius: 4px; + border: 1px solid var(--border); +} + +/* Noise gate: a header with a live value, a level meter, and a range slider + that shares the meter's horizontal (dB) axis. */ +.field-head { + display: flex; + align-items: baseline; + justify-content: space-between; +} +.field-value { + font-weight: 500; + font-size: 12px; + letter-spacing: 0; + text-transform: none; + color: var(--text-dim); +} +/* The slider and the level meter are one widget: the range input is overlaid + on the meter track (its native track is transparent), so the live-level fill + shows through behind the thumb and the thumb itself is the threshold. */ +.gate { + display: flex; + flex-direction: column; + gap: 4px; +} +.gate-track { + position: relative; + height: 14px; + display: flex; + align-items: center; +} +.gate-track::before { + /* the visible meter track */ + content: ""; + position: absolute; + left: 0; + right: 0; + height: 8px; + border-radius: 999px; + background: var(--bg); + border: 1px solid var(--border); +} +.gate-meter-fill { + position: absolute; + left: 1px; + top: 50%; + transform: translateY(-50%); + height: 6px; + width: 0; + border-radius: 999px; + background: var(--accent-2); + transition: width 0.06s linear; + pointer-events: none; +} +.gate-ends { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--text-faint); + letter-spacing: 0; + text-transform: none; +} +/* The range input sits transparently on top of the meter track. */ +.gate-track input[type="range"] { + position: relative; + z-index: 1; + width: 100%; + margin: 0; + -webkit-appearance: none; + appearance: none; + padding: 0; + border: none; + background: transparent; + height: 14px; + cursor: pointer; +} +.gate-track input[type="range"]::-webkit-slider-runnable-track { + height: 14px; + background: transparent; +} +.gate-track input[type="range"]::-moz-range-track { + height: 14px; + background: transparent; +} +.gate-track input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 6px; + height: 18px; + border-radius: 3px; + background: var(--text); + border: 2px solid var(--bg-elev); + box-shadow: 0 0 0 1px var(--border-strong); +} +.gate-track input[type="range"]::-moz-range-thumb { + width: 6px; + height: 18px; + border-radius: 3px; + background: var(--text); + border: 2px solid var(--bg-elev); + box-shadow: 0 0 0 1px var(--border-strong); +} +.gate-track input[type="range"]:focus { border: none; } + +.modal-footer { + display: flex; + justify-content: space-between; + gap: 12px; + padding-top: 6px; +} + +.btn { + padding: 10px 16px; + border-radius: var(--radius-sm); + border: 1px solid var(--border-strong); + background: var(--bg-elev-2); + color: var(--text); + font-weight: 600; + font-size: 13px; + cursor: pointer; + transition: background 0.15s, border-color 0.15s, transform 0.05s; +} +.btn:hover { + border-color: var(--text); +} +.btn:active { + transform: translateY(1px); +} +.btn.primary { + background: var(--text); + border-color: var(--text); + color: var(--bg); +} +.btn.primary:hover { + background: #fff; + border-color: #fff; +} +.btn.ghost { + background: transparent; + border-color: var(--border); + color: var(--text-dim); +} +.btn.wide { + width: 100%; + padding: 12px 16px; +} +.btn[disabled] { + opacity: 0.45; + cursor: not-allowed; +} +.btn[disabled]:hover { + border-color: var(--border-strong); +} + +/* ─── Settings tabs ───────────────────────────────────────────────────── */ + +.tabs { + display: flex; + gap: 4px; + padding: 4px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} +.tab { + flex: 1; + padding: 8px 12px; + border: none; + background: transparent; + color: var(--text-dim); + font-family: inherit; + font-size: 13px; + font-weight: 600; + letter-spacing: 0.02em; + border-radius: calc(var(--radius-sm) - 3px); + cursor: pointer; + transition: background 0.15s, color 0.15s; +} +.tab:hover { + color: var(--text); +} +.tab.active { + background: var(--bg-elev-2); + color: var(--text); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.tab-panels { + display: flex; + flex-direction: column; +} +.tab-panel { + display: flex; + flex-direction: column; + gap: 16px; +} +.tab-panel[hidden] { + display: none; +} + +/* Horizontal layout for Voice + Model. */ +.field-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +/* ─── Chat button + badge ────────────────────────────────────────────── */ + +#chat-btn { + position: relative; +} + +.chat-badge { + position: absolute; + top: 6px; + right: 6px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text); + border: 1.5px solid var(--bg-elev); + opacity: 0; + transform: scale(0); + transition: opacity 0.2s ease, transform 0.2s ease; + pointer-events: none; +} +.chat-badge.visible { + opacity: 1; + transform: scale(1); +} + +/* ─── Ephemeral bubble stack ─────────────────────────────────────────── */ + +.bubble-stack { + position: fixed; + /* Clear the topbar control row so the first bubble doesn't sit at the same + * height as the buttons. */ + top: 96px; + right: 28px; + width: min(300px, calc(100vw - 56px)); + display: flex; + flex-direction: column; + gap: 8px; + pointer-events: none; + z-index: 100; +} + +.bubble { + --bubble-dx: 10px; + pointer-events: auto; + padding: 10px 14px; + border-radius: var(--radius-md); + font-size: 13px; + line-height: 1.5; + border: 1px solid var(--border); + background: var(--bg-elev); + box-shadow: 0 4px 18px rgba(0, 0, 0, 0.32); + max-width: 100%; + word-break: break-word; + opacity: 0; + transform: translateX(var(--bubble-dx)); + transition: opacity 0.22s ease, transform 0.22s ease; +} +.bubble.in { + opacity: 1; + transform: translateX(0); +} +.bubble.out { + opacity: 0; + transform: translateX(var(--bubble-dx)); + pointer-events: none; + transition: opacity 0.3s ease, transform 0.3s ease; +} + +/* Surfaces stay neutral; the side they sit on plus the mono role label + * carry the distinction. No tinted fills — color is the orb's job. */ +.bubble.user { + --bubble-dx: -10px; + align-self: flex-start; +} +.bubble.assistant { + --bubble-dx: 10px; + align-self: flex-end; +} +.bubble.tool { + --bubble-dx: -10px; + align-self: flex-start; + display: flex; + align-items: center; + gap: 8px; + color: var(--text-dim); + font-family: var(--font-mono); + font-size: 12px; +} +.bubble.tool .bubble-tool-icon { + width: 13px; + height: 13px; + flex: none; + color: var(--voice-tool); +} + +.bubble-role { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 4px; + opacity: 0.7; +} +.bubble.user .bubble-role { color: var(--voice-user); } +.bubble.assistant .bubble-role { color: var(--voice-assistant); } + +/* ─── Conversation history panel ─────────────────────────────────────── */ + +.chat-panel { + position: fixed; + inset: 0; + z-index: 200; + pointer-events: none; +} +.chat-panel-backdrop { + position: absolute; + inset: 0; + background: rgba(8, 9, 13, 0.45); + backdrop-filter: blur(6px); + -webkit-backdrop-filter: blur(6px); + opacity: 0; + transition: opacity 0.25s ease; +} +.chat-panel.open .chat-panel-backdrop { + opacity: 1; + pointer-events: auto; +} +.chat-panel-inner { + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: min(360px, 90vw); + background: var(--bg-elev); + border-left: 1px solid var(--border); + display: flex; + flex-direction: column; + transform: translateX(100%); + transition: transform 0.28s cubic-bezier(0.32, 0.72, 0, 1); + pointer-events: auto; + box-shadow: -8px 0 32px rgba(0, 0, 0, 0.35); +} +.chat-panel.open .chat-panel-inner { + transform: translateX(0); +} +.chat-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 20px; + border-bottom: 1px solid var(--border); + flex: none; +} +.chat-panel-header h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + letter-spacing: 0.01em; +} +.chat-history { + flex: 1; + overflow-y: auto; + padding: 16px 20px; + display: flex; + flex-direction: column; + gap: 10px; + scroll-behavior: smooth; +} +.chat-history::-webkit-scrollbar { width: 3px; } +.chat-history::-webkit-scrollbar-track { background: transparent; } +.chat-history::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } + +.chat-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + color: var(--text-faint); + padding: 48px 0; + opacity: 0.65; +} +.chat-empty svg { + margin-bottom: 6px; + opacity: 0.8; +} +.chat-empty-title { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; +} +.chat-empty-hint { + font-size: 12px; + color: var(--text-faint); + opacity: 0.7; +} + +/* ─── History messages ───────────────────────────────────────────────── */ + +.hist-msg { + display: flex; + flex-direction: column; + gap: 3px; + max-width: 88%; +} +.hist-msg.user { align-self: flex-start; } +.hist-msg.assistant { align-self: flex-end; } +.hist-msg.tool { align-self: flex-start; max-width: 100%; } + +.hist-role { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); + padding: 0 2px; +} +.hist-msg.user .hist-role { color: color-mix(in srgb, var(--voice-user) 75%, var(--text-faint)); } +.hist-msg.assistant .hist-role { color: color-mix(in srgb, var(--voice-assistant) 75%, var(--text-faint)); } +.hist-msg.tool .hist-role { color: color-mix(in srgb, var(--voice-tool) 75%, var(--text-faint)); } + +.hist-body { + padding: 9px 12px; + border-radius: var(--radius-md); + font-size: 13px; + line-height: 1.5; + border: 1px solid var(--border); + background: var(--bg-elev-2); + word-break: break-word; +} +/* Bodies share one neutral surface; alignment + the mono role label do the + * distinguishing, so the panel reads as one quiet column. */ +.hist-msg.user .hist-body.partial { opacity: 0.65; } + +/* ─── Tool call history item ─────────────────────────────────────────── */ + +.hist-tool-header { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-radius: var(--radius-md) var(--radius-md) 0 0; + background: color-mix(in srgb, var(--processing) 10%, var(--bg-elev-2)); + border: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border)); + border-bottom: 1px solid color-mix(in srgb, var(--processing) 15%, var(--border)); + cursor: pointer; + color: var(--text); + font-family: inherit; + font-size: 13px; + font-weight: 500; + width: 100%; + text-align: left; + transition: background 0.15s; +} +.hist-tool-header:only-child { + border-radius: var(--radius-md); + border-bottom: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border)); +} +.hist-tool-header:hover { + background: color-mix(in srgb, var(--processing) 16%, var(--bg-elev-2)); +} +.hist-tool-icon { + width: 13px; + height: 13px; + flex: none; + color: var(--processing); +} +.hist-tool-name { + font-family: var(--font-mono); + font-size: 12px; + color: var(--voice-tool); + font-weight: 600; +} +.hist-tool-chevron { + margin-left: auto; + width: 13px; + height: 13px; + color: var(--text-faint); + transition: transform 0.2s ease; + flex: none; +} +.hist-tool-header[aria-expanded="true"] .hist-tool-chevron { + transform: rotate(180deg); +} +.hist-tool-body { + padding: 10px 12px 12px; + border-radius: 0 0 var(--radius-md) var(--radius-md); + background: var(--bg); + border: 1px solid color-mix(in srgb, var(--processing) 22%, var(--border)); + border-top: none; + display: none; +} +.hist-tool-body.open { display: block; } +.hist-tool-label { + font-family: var(--font-mono); + font-size: 10px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--text-faint); + margin: 8px 0 4px; +} +.hist-tool-label:first-child { margin-top: 0; } +.hist-tool-block { + font-family: var(--font-mono); + font-size: 11px; + line-height: 1.65; + color: var(--text-dim); + white-space: pre-wrap; + word-break: break-word; + overflow-x: auto; +} +.hist-tool-output { color: var(--text); } + +/* ─── Phone layout ──────────────────────────────────────────────────────── + * On phones the floating bubble stream overlaps the orb and there isn't room + * for it, so we drop it entirely and rely on the conversation panel (opened + * from the top-right chat button) as the single place to read the transcript. + * The badge still pulses there when new messages arrive while it's closed. + * We also stack the mic / stop controls above and below the orb (instead of + * left/right) so the wide live row never overflows, and let the panel take + * the full width. */ +@media (max-width: 600px) { + .bubble-stack { + display: none; + } + + .topbar { + padding: 14px 16px; + } + + .stage { + padding: 16px 12px 24px; + } + + /* Stack vertically: mic above the orb, stop below it (DOM order is + * mic → circle → stop). */ + .orb-wrap { + flex-direction: column; + gap: 14px; + } + + .circle { + /* Height-capped like the base rule so a narrow AND short box still fits. */ + width: clamp(130px, min(56vw, 52vh), 240px); + } + + /* In the column layout the side controls must collapse by HEIGHT, not + * width, so they take no vertical space until the session is live. */ + .side-btn { + width: 44px; + height: 0; + transition: opacity 0.25s ease, transform 0.25s ease, height 0.25s ease, + background 0.15s, color 0.15s, border-color 0.15s; + } + .side-btn svg { + width: 19px; + height: 19px; + } + .orb-wrap.live .side-btn { + width: 44px; + height: 44px; + } + .mic-gate-arc { width: 70px; height: 70px; } + + /* Full-screen conversation on phones — feels more deliberate than a + * narrow slide-over. It already spans top-to-bottom (inset 0); make it + * span edge-to-edge too and drop the now-pointless border/shadow. */ + .chat-panel-inner { + width: 100vw; + border-left: none; + box-shadow: none; + } +} + +/* ─── About panel ───────────────────────────────────────────────────────── + * Opened from the (i) by the wordmark. Reuses the .modal shell. + * Strictly monochrome per DESIGN.md — the only "color" is the orb, never + * here. Machine identifiers (model IDs, role tags, usernames) ride Geist + * Mono; everything human stays Inter. */ +.about-modal { + /* Roomier on tablet/desktop; the 92vw cap keeps phones full-width. */ + width: min(680px, 92vw); + max-height: 88vh; +} +.about-modal .modal-content { + max-height: 88vh; + overflow-y: auto; + gap: 20px; + padding: 26px 30px 24px; +} +.about-modal .modal-content::-webkit-scrollbar { width: 3px; } +.about-modal .modal-content::-webkit-scrollbar-track { background: transparent; } +.about-modal .modal-content::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 3px; } + +/* Links here are quiet: no permanent underline (the global dotted rule is + * too loud for a dense credit block), brighten + underline on hover only. */ +.about-modal a { + color: var(--text); + border-bottom: none; + text-decoration: none; + transition: color 0.15s ease; +} +.about-modal a:hover { + color: #fff; +} +.about-intro a:hover { + text-decoration: underline; + text-underline-offset: 2px; +} +.about-modal .ext { + width: 12px; + height: 12px; + flex: none; + opacity: 0.5; +} + +/* Title row: the demo name and the (i) read as one unit. */ +.ident-head { + display: flex; + align-items: center; + gap: 9px; +} +/* (i) trigger sits right after the title. A faint outline keeps it + * catchable without turning into a card; it fills in on hover. */ +.about-btn { + flex: none; + width: 34px; + height: 34px; + border-radius: 50%; + background: transparent; + border: 1px solid var(--border-strong); + color: var(--text-dim); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + touch-action: manipulation; + transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease; +} +.about-btn:hover { + background: var(--bg-elev); + border-color: var(--text-dim); + color: var(--text); +} +.about-btn svg { + width: 20px; + height: 20px; +} + +/* The mobile twin of (i) lives in the right-hand control cluster and is + * hidden on desktop (the in-title one shows there instead). */ +.about-btn-mobile { + display: none; +} + +/* ── Popup intro: a plain paragraph on the project + a repo link ── */ +.about-intro p { + margin: 0; + font-size: 14px; + line-height: 1.6; + color: var(--text-dim); +} +.about-repo { + display: inline-flex; + align-items: center; + gap: 5px; + margin-top: 12px; + font-size: 14px; + font-weight: 500; +} + +/* ── Corner identity (replaces the wordmark) ── + * A compact stack in the topbar: title, one-line blurb, two meta rows. + * Monochrome; only the role/name identifiers ride the machine typeface. */ +.ident { + display: flex; + flex-direction: column; + gap: 10px; + min-width: 0; +} +/* Reset the global underlined-anchor styling inside the identity block. */ +.ident a { + border-bottom: none; + color: inherit; + transition: color 0.15s ease; +} +.ident a:hover { + color: var(--text); + text-decoration: underline; + text-underline-offset: 2px; +} +.ident-title { + font-size: 22px; + font-weight: 600; + letter-spacing: 0.005em; + line-height: 1.1; + color: var(--text); +} +.ident-blurb { + margin: 0; + max-width: 48ch; + font-size: 14.5px; + line-height: 1.5; + font-weight: 400; + color: var(--text-dim); +} +.ident-meta { + display: flex; + flex-direction: column; + gap: 7px; + font-size: 14px; + font-weight: 400; + color: var(--text-dim); +} +.ident-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 4px 8px; +} +.ident-label { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); +} + +/* Shared credit bits — now used in the corner identity block. */ +.sep { + color: var(--text-faint); + opacity: 0.6; +} +.hf-credit { + display: inline-flex; + align-items: center; + gap: 5px; +} +/* Brand marks keep their own color — a deliberate exception to the + * monochrome rule, for the HF and Cerebras logos only. */ +.hf-mark { + width: 14px; + height: 14px; + flex: none; + color: #ffd21e; +} +.cerebras-credit { + display: inline-flex; + align-items: center; + gap: 5px; +} +.cerebras-mark { + width: 14px; + height: 14px; + flex: none; +} +/* Usernames are identifiers, so they ride the machine typeface. */ +.handle { + font-family: var(--font-mono); + font-size: 13.5px; +} + +/* ── Pipeline (the signal flow) ── */ +.about-pipeline { + border-top: 1px solid var(--border); + padding-top: 16px; +} +.pipeline-title { + margin: 0 0 14px; + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text-faint); +} +.pipeline { + list-style: none; + margin: 0; + padding: 0; + position: relative; +} +/* One continuous rail threading every node, dot centers at x=8.5px. */ +.pipeline::before { + content: ""; + position: absolute; + left: 8px; + top: 7px; + bottom: 7px; + width: 1px; + background: var(--border-strong); +} +.pipeline > li { + position: relative; + padding-left: 30px; +} +/* Stages: solid node. Endpoints (you / orb): hollow node. */ +.pipe-stage::before { + content: ""; + position: absolute; + left: 5px; + top: 4px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--text-dim); +} +.pipe-endpoint::before { + content: ""; + position: absolute; + left: 5px; + top: 3px; + width: 7px; + height: 7px; + border-radius: 50%; + border: 1px solid var(--text-faint); + background: var(--bg-elev); +} +.pipe-endpoint { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 500; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--text-faint); + padding-bottom: 12px; +} +.pipeline > li.pipe-endpoint:last-child { + padding-bottom: 0; +} +.pipe-stage { + padding-bottom: 16px; +} +.pipe-tag { + font-family: var(--font-mono); + font-size: 12px; + font-weight: 600; + letter-spacing: 0.08em; + color: var(--text); + margin-right: 9px; +} +.pipe-job { + font-size: 14px; + color: var(--text-dim); +} +/* Middot between the job and its model link, matching the separators + * used elsewhere. */ +.pipe-job::after { + content: "·"; + margin: 0 7px; + color: var(--text-faint); +} +.pipe-note { + color: var(--text-faint); +} +/* The Cerebras link stays as quiet as the note; brightens + underlines on hover. */ +.pipe-note a { + color: inherit; +} +.pipe-note a:hover { + color: var(--text); + text-decoration: underline; + text-underline-offset: 2px; +} +.pipe-model { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 4px; + font-family: var(--font-mono); + font-size: 12px; + color: var(--text-dim); + word-break: break-all; +} +.pipe-model:hover { + color: #fff; + text-decoration: underline; + text-underline-offset: 2px; +} +.pipe-model .ext { + width: 11px; + height: 11px; +} + +/* "Interrupted" tag on an assistant reply the user barged in on. */ +.hist-note { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 500; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--text-faint); + padding: 0 2px; +} +.hist-msg.assistant .hist-note { + align-self: flex-end; +} +.hist-msg.interrupted .hist-body { + opacity: 0.7; +} + +/* Captured webcam frame shown in the transcript (camera tool result). */ +.hist-image { + display: block; + width: 100%; + max-width: 240px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + margin-top: 2px; +} + +/* ─── Tools panel ────────────────────────────────────────────────────────── + * Reuses the modal/field shell. Switches are monochrome (checked = near-white, + * the same high-contrast treatment as the primary button): color belongs to + * the voice, not the chrome. */ +.tools-intro { + margin: 0; + font-size: 13px; + line-height: 1.5; + color: var(--text-dim); +} +.tool-list { + display: flex; + flex-direction: column; +} +.tool-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 12px 0; +} +.tool-row-sep { + border-top: 1px solid var(--border); + margin-top: 4px; +} +.tool-info { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; +} +.tool-name { + font-size: 14px; + font-weight: 600; + color: var(--text); +} +.tool-desc { + font-size: 12.5px; + color: var(--text-dim); +} +.tool-row.disabled .tool-name, +.tool-row.disabled .tool-desc { + opacity: 0.5; +} +.tool-hint { + display: block; + font-size: 12px; + line-height: 1.4; + color: var(--text-faint); +} +.tools-key { + margin: 0 0 4px; +} + +/* Toggle switch */ +.switch { + position: relative; + display: inline-flex; + flex: none; + width: 40px; + height: 24px; + cursor: pointer; +} +.switch input { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + margin: 0; + opacity: 0; + cursor: pointer; +} +.switch-track { + position: absolute; + inset: 0; + border-radius: 999px; + background: var(--bg); + border: 1px solid var(--border-strong); + transition: background 0.15s, border-color 0.15s; +} +.switch-track::after { + content: ""; + position: absolute; + top: 50%; + left: 3px; + transform: translateY(-50%); + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--text-dim); + transition: transform 0.18s ease, background 0.15s; +} +.switch input:checked + .switch-track { + background: var(--text); + border-color: var(--text); +} +.switch input:checked + .switch-track::after { + transform: translate(16px, -50%); + background: var(--bg); +} +.switch input:focus-visible + .switch-track { + outline: 2px solid var(--text-dim); + outline-offset: 2px; +} +.switch input:disabled { + cursor: not-allowed; +} +.switch input:disabled + .switch-track { + opacity: 0.5; +} + +/* ─── Webcam preview (camera tool) ────────────────────────────────────────── + * Floating self-view, bottom-left. Mirrored for the user; the frame sent to + * the model is drawn un-mirrored (see captureSnapshot). */ +.cam-pip { + position: fixed; + left: 20px; + bottom: 20px; + width: 280px; + aspect-ratio: 4 / 3; + border-radius: var(--radius-md); + overflow: hidden; + border: 1px solid var(--border-strong); + background: var(--bg-elev); + box-shadow: var(--shadow-soft); + z-index: 90; + opacity: 0; + transform: translateY(8px) scale(0.96); + pointer-events: none; + transition: opacity 0.22s ease, transform 0.22s ease; +} +.cam-pip.visible { + opacity: 1; + transform: none; +} +.cam-video { + display: block; + width: 100%; + height: 100%; + object-fit: cover; + transform: scaleX(-1); /* mirror the self-view only */ + background: var(--bg); +} +.cam-label { + position: absolute; + left: 8px; + bottom: 6px; + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--text); + opacity: 0.8; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.7); +} +.cam-flash { + position: absolute; + inset: 0; + background: #fff; + opacity: 0; + pointer-events: none; +} +.cam-pip.flash .cam-flash { + animation: cam-flash 0.4s ease; +} +@keyframes cam-flash { + 0% { opacity: 0; } + 12% { opacity: 0.85; } + 100% { opacity: 0; } +} +@media (max-width: 600px) { + /* Bottom-centred on phones. Auto margins centre it without touching the + * transform, so the slide/scale-in animation still works. */ + .cam-pip { + left: 0; + right: 0; + margin-inline: auto; + bottom: 16px; + width: min(188px, 52vw); + } + /* The credit would sit under the centred preview, so drop it while the + * camera is on. */ + body.cam-on .footer { + display: none; + } +} +@media (prefers-reduced-motion: reduce) { + .cam-pip { + transition: opacity 0.22s ease; + transform: none; + } + .cam-pip.flash .cam-flash { + animation: none; + } +} + +/* ─── Desktop type scale ──────────────────────────────────────────────────── + * A uniform step up (~+1px, title +2) for all UI text on larger screens. + * Scoped to min-width: 601px so it can't reach phones — the ≤600px layout + * keeps every size exactly as it was. */ +@media (min-width: 601px) { + .cam-label { font-size: 10.5px; } + + .bubble-role, + .hist-role { font-size: 11px; } + + .circle-caption, + .footer, + .chat-empty-title, + .hist-tool-block, + .ident-label, + .pipeline-title, + .pipe-endpoint { font-size: 12px; } + + .field > span, + .field small, + .bubble.tool, + .chat-empty-hint, + .hist-tool-name, + .pipe-tag, + .pipe-model, + .tool-hint { font-size: 13px; } + + .tool-desc { font-size: 13.5px; } + + .btn, + .bubble, + .field, + .hist-body, + .hist-tool-header, + .tools-intro { font-size: 14px; } + + .handle { font-size: 14.5px; } + + .about-intro p, + .about-repo, + .brand, + .chat-panel-header h3, + .field textarea, + .ident-meta, + .pipe-job, + .tool-name { font-size: 15px; } + + .ident-blurb { font-size: 15.5px; } + + .modal-header h2 { font-size: 17px; } + + .ident-title { font-size: 24px; } +} + +/* ─── Short viewports: fit without a vertical scrollbar ───────────────────── + * The base media queries only react to WIDTH, so a short container (e.g. the + * app embedded in a small box, or a laptop window once the browser chrome eats + * into the viewport) used to overflow vertically and get clipped. We KEEP the + * identity header, orb, caption and credits — we just undo the desktop type + * bump, tighten the vertical rhythm and let the height-capped orb shrink, so + * everything fits with comfortable margin. No opt-in needed. + * + * The single biggest space eater is the identity block (~180px at the desktop + * type scale), so this tier compacts it first. */ +@media (max-height: 780px) { + .topbar { padding: 14px 24px; } + .ident { gap: 5px; } + .ident-title { font-size: 20px; } + .ident-blurb { font-size: 13.5px; line-height: 1.4; } + .ident-meta { gap: 5px; font-size: 13px; } + .stage { padding: 12px 20px 16px; gap: 12px; } + .footer { padding: 8px 24px 10px; } + .circle { width: clamp(140px, min(38vw, 44vh), 300px); } +} + +@media (max-height: 560px) { + .topbar { padding: 8px 16px; } + .ident-title { font-size: 17px; } + /* Below this, room is tight enough that the one-line blurb and the footer + * (a verbatim duplicate of the header's "Powered by" credit) are dropped so + * the title + credits still read; the rest of the header stays. */ + .ident-blurb { display: none; } + .stage { padding: 8px 14px 10px; gap: 8px; } + .footer { display: none; } + .circle { width: clamp(120px, min(50vw, 46vh), 240px); } +} diff --git a/backend/vendor/s2s-cloud/demo/textchat.html b/backend/vendor/s2s-cloud/demo/textchat.html new file mode 100644 index 0000000..561f41c --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/textchat.html @@ -0,0 +1,154 @@ + + + + + +文本对话 · 云 LLM/STT/TTS(免麦克风) + + + +

💬 文本对话 · 云 LLM(qwen-plus) + 云 TTS(qwen3-tts)(免麦克风)

+
连接中…
+
+
+ + + +
+
回复语音在「发送」点击后自动播放(浏览器要求手势内激活音频);没听到就点「重播」。
+ + + + diff --git a/backend/vendor/s2s-cloud/demo/ui/account.js b/backend/vendor/s2s-cloud/demo/ui/account.js new file mode 100644 index 0000000..dc2d5b2 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/ui/account.js @@ -0,0 +1,189 @@ +// @ts-check +/** + * Account — the HF login chip and the daily-limit modal. + * + * Reads `/api/me` to learn the current tier (anonymous / signed-in / PRO) and + * remaining daily talk-time, renders a sign-in pill or a signed-in chip with a + * small popover (tier, remaining, sign out, upgrade), and shows the limit modal + * when a conversation is refused or cut. The time metering itself lives in + * main.js (heartbeat loop) + the server; this module is just the surface. + * + * Inert unless the deploy is in LB mode (`/api/me` → `{enabled:true}`). + */ + +import { $, escHtml } from "./dom.js"; + +const PRO_URL = "https://huggingface.co/subscribe/pro"; + +// Official multi-color Hugging Face logo, used in the badge + sign-in CTA. +const HF_MARK = ``; + +/** @param {number} sec @returns {string} "m:ss" */ +function fmt(sec) { + const s = Math.max(0, Math.round(sec)); + return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; +} + +export class Account { + constructor() { + /** @type {HTMLElement} */ + this._root = $("#account"); + /** @type {HTMLDialogElement} */ + this._modal = $("#limit-modal"); + this._modalTitle = $("#limit-title"); + this._modalMsg = $("#limit-msg"); + this._modalNote = $("#limit-note"); + /** @type {HTMLAnchorElement} */ + this._modalCta = /** @type {any} */ ($("#limit-cta")); + + /** @type {{enabled:boolean, auth?:boolean, loggedIn?:boolean, username?:string, avatar?:string, tier?:string, remainingSec?:number|null, limitSec?:number|null, loginUrl?:string|null, logoutUrl?:string|null}} */ + this._me = { enabled: false }; + this._popoverOpen = false; + + $("#limit-close").addEventListener("click", () => this._modal.close()); + this._modal.addEventListener("click", (e) => { + if (e.target === this._modal) this._modal.close(); + }); + // Close the popover on an outside click. + document.addEventListener("click", (e) => { + if (this._popoverOpen && !this._root.contains(/** @type {Node} */ (e.target))) { + this._closePopover(); + } + }); + } + + get tier() { + return this._me.tier || "anon"; + } + + /** Fetch `/api/me` and (re)render the chip. Safe to call repeatedly (load, + * after the OAuth redirect, after a conversation ends). */ + async refresh() { + try { + const res = await fetch("api/me"); + this._me = res.ok ? await res.json() : { enabled: false }; + } catch { + this._me = { enabled: false }; + } + this._render(); + } + + _render() { + const me = this._me; + if (!me.enabled) { + this._root.hidden = true; + this._root.innerHTML = ""; + return; + } + this._root.hidden = false; + + if (!me.loggedIn) { + // Signed-out: a sign-in pill (only when OAuth is actually available). + if (me.auth && me.loginUrl) { + this._root.innerHTML = ``; + } else { + this._root.innerHTML = ""; + this._root.hidden = true; + } + return; + } + + // Signed-in: avatar + handle chip that toggles a popover. + const isPro = me.tier === "pro"; + // Org members get unlimited usage too, but aren't PRO — don't brand them so. + const isUnlimited = isPro || me.tier === "org"; + const avatar = me.avatar + ? `` + : ``; + const remaining = + isUnlimited || me.remainingSec == null + ? "Unlimited" + : `${fmt(me.remainingSec)} left today`; + const tierLabel = isPro ? "PRO" : isUnlimited ? "Team" : "Free"; + + this._root.innerHTML = ` + + `; + + const chip = $("#account-chip"); + chip.addEventListener("click", (e) => { + e.stopPropagation(); + this._popoverOpen ? this._closePopover() : this._openPopover(); + }); + } + + _openPopover() { + const pop = document.getElementById("account-pop"); + const chip = document.getElementById("account-chip"); + if (!pop || !chip) return; + pop.hidden = false; + chip.setAttribute("aria-expanded", "true"); + this._popoverOpen = true; + } + + _closePopover() { + const pop = document.getElementById("account-pop"); + const chip = document.getElementById("account-chip"); + if (pop) pop.hidden = true; + if (chip) chip.setAttribute("aria-expanded", "false"); + this._popoverOpen = false; + } + + /** + * Show the limit modal for a tier — used both when a conversation is refused + * at start (402) and when a live one is cut (heartbeat `expired`). + * @param {string} [tier] + */ + showLimit(tier = this.tier) { + const canSignIn = this._me.auth && this._me.loginUrl; + this._modalTitle.textContent = "Thanks for chatting!"; + if (tier === "anon") { + this._modalMsg.textContent = + "Guest conversations run for 5 minutes. Sign in with Hugging Face to get 10 minutes a day for free, and PRO members chat with no limit at all."; + this._modalNote.textContent = "Your free minutes refresh tomorrow."; + if (canSignIn) { + this._modalCta.innerHTML = `${HF_MARK}Sign in with Hugging Face`; + this._modalCta.href = /** @type {string} */ (this._me.loginUrl); + this._modalCta.hidden = false; + } else { + this._modalCta.hidden = true; + } + } else { + // Signed-in, non-PRO. + this._modalMsg.textContent = + "You've enjoyed your 10 minutes for today. Go PRO for unlimited conversations and to support open source AI."; + this._modalNote.textContent = "Or come back tomorrow. Your minutes reset daily."; + this._modalCta.innerHTML = "Upgrade to PRO"; + this._modalCta.href = PRO_URL; + this._modalCta.hidden = false; + } + if (!this._modal.open) this._modal.showModal(); + } + + /** Show a warm "we're at capacity" message when even the waiting line is full. + * Reuses the limit modal shell; no call-to-action, just reassurance. */ + showBusy() { + this._modalTitle.textContent = "Hugged to the limit 🤗"; + this._modalMsg.textContent = + "Every slot and the whole line are full right now. Too much love! Grab a coffee and pop back in a minute."; + this._modalNote.textContent = "A spot usually opens up within a few minutes."; + this._modalCta.hidden = true; + if (!this._modal.open) this._modal.showModal(); + } +} diff --git a/backend/vendor/s2s-cloud/demo/ui/chat.js b/backend/vendor/s2s-cloud/demo/ui/chat.js new file mode 100644 index 0000000..b54a672 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/ui/chat.js @@ -0,0 +1,422 @@ +// @ts-check +/** + * ChatView — owns the whole conversation surface: the slide-in history panel, + * the ephemeral on-orb bubbles, and all the transcript/tool/streaming + * bookkeeping. main.js wires the realtime client's events straight to the + * `on*` methods here and otherwise doesn't touch chat state. + * + * Two parallel surfaces share one shape (see `_buildMessageEl`): + * - ephemeral bubbles (`.bubble` / `.bubble-*`) fade on a timer + * - persistent history (`.hist-msg` / `.hist-*`) the durable panel log + * + * Keying: + * - user transcripts by the server's `item_id` — a speculative continuation + * REUSES it, so both segments land in one row/bubble; deltas are CUMULATIVE + * (each carries the full sentence so far), so we replace text wholesale. + * - assistant transcripts by `response_id`, so a cancelled speculative reply + * can be marked interrupted without erasing what was already shown. + */ + +import { $, escHtml, DEBUG } from "./dom.js"; + +const WRENCH_PATH = ``; +const CHAT_BUBBLE_SVG = ``; +const EMPTY_STATE_HTML = `
${CHAT_BUBBLE_SVG}No messages yetTap the orb and start talking
`; + +export class ChatView { + constructor() { + /** @type {HTMLButtonElement} */ + this._chatBtn = $("#chat-btn"); + /** @type {HTMLSpanElement} */ + this._chatBadge = $("#chat-badge"); + /** @type {HTMLDivElement} */ + this._chatPanel = $("#chat-panel"); + /** @type {HTMLDivElement} */ + this._chatPanelBackdrop = $("#chat-panel-backdrop"); + /** @type {HTMLButtonElement} */ + this._chatPanelClose = $("#chat-panel-close"); + /** @type {HTMLDivElement} */ + this._chatHistory = $("#chat-history"); + /** @type {HTMLDivElement} */ + this._bubbleStack = $("#bubble-stack"); + + this._panelOpen = false; + this._scrollQueued = false; + + // ── User transcript state (keyed by item_id) ─────────────────────────── + /** @type {Map} */ + this._userHistByItem = new Map(); + /** @type {HTMLElement | null} */ + this._activeUserBubble = null; + this._activeUserItemId = ""; + // Monotonic counter for synthesizing unique keys when the server omits an + // item_id / response_id, so id-less messages never collapse onto each other. + this._anonSeq = 0; + + // ── Assistant transcript state (keyed by response_id) ────────────────── + /** @type {Map} */ + this._asstByResp = new Map(); + + // ── Ephemeral bubble auto-dismiss ────────────────────────────────────── + // Per-element expiry (epoch ms). A bubble fades once its expiry passes — + // but only in stack order (see _reapBubbles). Refreshing the expiry keeps a + // bubble alive while it updates (e.g. the live user utterance). + /** @type {WeakMap} */ + this._bubbleExpiry = new WeakMap(); + // Single pending reaper handle: one timer for the whole stack (not one per + // bubble) so dismissal is strictly oldest-first regardless of per-bubble delays. + this._reaperHandle = 0; + + this._chatBtn.addEventListener("click", () => (this._panelOpen ? this._closePanel() : this._openPanel())); + this._chatPanelClose.addEventListener("click", () => this._closePanel()); + this._chatPanelBackdrop.addEventListener("click", () => this._closePanel()); + document.addEventListener("keydown", (e) => { + if (e.key === "Escape" && this._panelOpen) this._closePanel(); + }); + } + + // ── Panel ─────────────────────────────────────────────────────────────── + + _openPanel() { + this._panelOpen = true; + this._chatPanel.classList.add("open"); + this._chatBadge.classList.remove("visible"); + this._scrollToBottom(); + } + + _closePanel() { + this._panelOpen = false; + this._chatPanel.classList.remove("open"); + } + + // Coalesce scroll-to-bottom: a burst of cumulative transcript deltas would + // otherwise queue one rAF per delta, all writing the same scrollTop. + _scrollToBottom() { + if (!this._panelOpen || this._scrollQueued) return; + this._scrollQueued = true; + requestAnimationFrame(() => { + this._scrollQueued = false; + this._chatHistory.scrollTop = this._chatHistory.scrollHeight; + }); + } + + _markUnread() { + if (this._panelOpen) { + this._scrollToBottom(); + return; + } + this._chatBadge.classList.add("visible"); + } + + // ── Shared rendering ────────────────────────────────────────────────────── + + /** + * Build a role-labelled message element. Ephemeral bubbles and persistent + * history rows share the same shape and differ only in their class prefix + * (`bubble`/`bubble-*` vs `hist-msg`/`hist-*`). + * @param {{ container: string, prefix: string, role: "user"|"assistant", text: string, partial?: boolean }} o + * @returns {HTMLElement} + */ + _buildMessageEl({ container, prefix, role, text, partial = false }) { + const el = document.createElement("div"); + el.className = `${container} ${role}`; + const label = role === "user" ? "You" : "Assistant"; + el.innerHTML = `
${label}
${escHtml(text)}
`; + return el; + } + + // ── Ephemeral bubbles ─────────────────────────────────────────────────── + + /** @param {"user"|"assistant"|"tool"} role @param {string} text @returns {HTMLElement} */ + _spawnBubble(role, text) { + let el; + if (role === "tool") { + el = document.createElement("div"); + el.className = "bubble tool"; + el.innerHTML = `${WRENCH_PATH}${escHtml(text)}`; + } else { + el = this._buildMessageEl({ container: "bubble", prefix: "bubble", role, text }); + } + this._bubbleStack.appendChild(el); + // Cap the stack at 8, but never evict the bubble the caller is still + // actively updating (the live user bubble) — drop the next-oldest instead. + const visible = /** @type {HTMLElement[]} */ ([...this._bubbleStack.querySelectorAll(".bubble:not(.out)")]); + if (visible.length > 8) { + this._dismissBubble(visible.find((b) => b !== this._activeUserBubble) ?? visible[0]); + } + requestAnimationFrame(() => el.classList.add("in")); + return el; + } + + /** @param {HTMLElement} el @param {string} text */ + _updateBubbleText(el, text) { + const t = el.querySelector(".bubble-body"); + if (t) t.textContent = text; + } + + /** @param {HTMLElement} el */ + _dismissBubble(el) { + if (!el || el.classList.contains("out")) return; // idempotent + this._bubbleExpiry.delete(el); + el.classList.remove("in"); + el.classList.add("out"); + const remove = () => el.remove(); + el.addEventListener("transitionend", remove, { once: true }); + // Fallback: transitionend never fires if the bubble's visual state didn't + // change (dismissed pre-paint) or the tab is backgrounded. The transition + // is 0.3s, so force removal a little after. + setTimeout(remove, 400); + } + + /** + * Fade bubbles whose expiry has passed — strictly oldest-first. We walk the + * stack top (oldest) to bottom (newest) and stop at the first bubble still + * alive: nothing newer may leave while an older bubble is still on screen. A + * bubble that keeps updating pushes its own expiry forward, so it (and + * everything behind it) stays put until it finally goes quiet. + */ + _reapBubbles() { + this._reaperHandle = 0; + const now = Date.now(); + const visible = /** @type {HTMLElement[]} */ ([...this._bubbleStack.querySelectorAll(".bubble:not(.out)")]); + let nextWake = Infinity; + for (const el of visible) { + const exp = this._bubbleExpiry.get(el) ?? now; // no expiry recorded → treat as due + if (exp <= now) { + this._dismissBubble(el); + } else { + // Oldest survivor isn't due yet; stop so nothing newer leaves before it. + nextWake = exp; + break; + } + } + if (nextWake !== Infinity) { + this._reaperHandle = setTimeout(() => this._reapBubbles(), Math.max(50, nextWake - Date.now())); + } + } + + /** + * (Re)arm a bubble's auto-dismiss by pushing its expiry out by `delay`. + * Calling it again resets the countdown — so a bubble that keeps updating + * stays on screen and only fades once it goes quiet. Removal is ordered by the + * shared reaper, so the oldest bubble always disappears first. + * @param {HTMLElement} el @param {number} [delay] + */ + _bumpDismiss(el, delay = 4000) { + this._bubbleExpiry.set(el, Date.now() + delay); + if (!this._reaperHandle) this._reaperHandle = setTimeout(() => this._reapBubbles(), delay); + } + + // ── History ─────────────────────────────────────────────────────────────── + + /** Render the empty-state placeholder into the history panel. */ + renderEmptyState() { + this._chatHistory.innerHTML = EMPTY_STATE_HTML; + } + + /** Reset the panel to the empty state and clear the unread badge. */ + clear() { + this.renderEmptyState(); + this._chatBadge.classList.remove("visible"); + } + + /** @param {"user"|"assistant"} role @param {string} text @param {boolean} partial @returns {HTMLElement} */ + _appendHistMsg(role, text, partial) { + const empty = this._chatHistory.querySelector(".chat-empty"); + if (empty) empty.remove(); + const el = this._buildMessageEl({ container: "hist-msg", prefix: "hist", role, text, partial }); + this._chatHistory.appendChild(el); + this._scrollToBottom(); + return el; + } + + /** @param {HTMLElement | null} el @param {string} text @param {boolean} partial */ + _updateHistMsg(el, text, partial) { + if (!el) return; + const body = /** @type {HTMLElement | null} */ (el.querySelector(".hist-body")); + if (!body) return; + body.textContent = text; + body.classList.toggle("partial", partial); + this._scrollToBottom(); + } + + /** + * Append a tool-call row to the conversation. We only add it once the tool + * has run, so the expandable toggle carries BOTH the call input and its result. + * @param {string} name @param {string} argsJson @param {string} output + */ + _appendHistTool(name, argsJson, output) { + const empty = this._chatHistory.querySelector(".chat-empty"); + if (empty) empty.remove(); + let pretty = argsJson; + try { pretty = JSON.stringify(JSON.parse(argsJson), null, 2); } catch {} + const el = document.createElement("div"); + el.className = "hist-msg tool"; + el.innerHTML = ` +
Tool call
+ +
+
Input
+
${escHtml(pretty)}
+
Output
+
${escHtml(output || "(no output)")}
+
+ `; + const header = /** @type {HTMLButtonElement} */ (el.querySelector(".hist-tool-header")); + const body = /** @type {HTMLDivElement} */ (el.querySelector(".hist-tool-body")); + header.addEventListener("click", () => { + const expanded = header.getAttribute("aria-expanded") === "true"; + header.setAttribute("aria-expanded", String(!expanded)); + body.classList.toggle("open", !expanded); + }); + this._chatHistory.appendChild(el); + this._scrollToBottom(); + } + + /** Tag an assistant history row as interrupted (user barged in mid-reply). + * @param {HTMLElement | null} hist */ + _markHistInterrupted(hist) { + if (!hist || hist.querySelector(".hist-note")) return; + hist.classList.add("interrupted"); + const note = document.createElement("div"); + note.className = "hist-note"; + note.textContent = "Interrupted"; + hist.appendChild(note); + } + + /** Render a captured webcam frame in the transcript (the camera tool result). + * @param {string} dataUrl */ + _appendHistImage(dataUrl) { + const empty = this._chatHistory.querySelector(".chat-empty"); + if (empty) empty.remove(); + const el = document.createElement("div"); + el.className = "hist-msg tool"; + el.innerHTML = `
Snapshot
Webcam snapshot sent to the model`; + const img = /** @type {HTMLImageElement} */ (el.querySelector("img")); + img.src = dataUrl; + this._chatHistory.appendChild(el); + this._scrollToBottom(); + } + + /** + * Reset all streaming bookkeeping for session start / teardown. Pass + * `dismiss` to also fade any bubbles still on screen. + * @param {{ dismiss?: boolean }} [opts] + */ + reset(opts) { + if (opts?.dismiss) { + if (this._activeUserBubble) this._dismissBubble(this._activeUserBubble); + for (const { bubble } of this._asstByResp.values()) this._dismissBubble(bubble); + } + this._userHistByItem.clear(); + this._activeUserBubble = null; + this._activeUserItemId = ""; + this._asstByResp.clear(); + } + + // ── Client event handlers ───────────────────────────────────────────────── + + /** + * A streamed transcript delta (user or assistant). + * @param {{ role: "user" | "assistant"; text: string; partial: boolean; itemId?: string; responseId?: string }} d + */ + onTranscript(d) { + if (DEBUG) console.debug(`[ui] transcript role=${d.role} partial=${d.partial} item=${d.itemId} resp=${d.responseId} text=${JSON.stringify(d.text)}`); + + if (d.role === "user") { + // Group by item_id: a speculative continuation reuses the same id, so it + // updates the same row/bubble. A missing id falls back to the active item + // (same utterance) or a fresh unique key, never a shared sentinel that + // would collapse distinct turns into one row. + const id = d.itemId || this._activeUserItemId || `_u${++this._anonSeq}`; + const text = d.text; + + let hist = this._userHistByItem.get(id); + if (!hist) { + hist = this._appendHistMsg("user", text, d.partial); + this._userHistByItem.set(id, hist); + } else { + this._updateHistMsg(hist, text, d.partial); + } + + // One ephemeral bubble per active item. Purely timer-based: the timer is + // refreshed on every delta, so it stays while the user keeps talking and + // fades a few seconds after they stop — no dependency on a response ever + // arriving, so it can never get stuck. + if (this._activeUserItemId !== id || !this._activeUserBubble) { + this._activeUserBubble = this._spawnBubble("user", text); + this._activeUserItemId = id; + } else { + this._updateBubbleText(this._activeUserBubble, text); + } + this._markUnread(); + } else if (d.role === "assistant") { + // Assistant transcript arrives once, as the full text, keyed by + // response_id so a cancelled speculative response can be removed later. A + // missing id gets a unique key so two id-less replies never collide. + const rid = d.responseId || `_a${++this._anonSeq}`; + const entry = this._asstByResp.get(rid); + if (!entry) { + const bubble = this._spawnBubble("assistant", d.text); + this._asstByResp.set(rid, { bubble, hist: this._appendHistMsg("assistant", d.text, false) }); + } else { + this._updateBubbleText(entry.bubble, d.text); + this._updateHistMsg(entry.hist, d.text, false); + } + this._markUnread(); + } + } + + /** + * A response closed (completed or cancelled). + * @param {{ responseId: string; status: string; audible?: boolean; transcript?: string }} detail + */ + onResponseFinished(detail) { + const { responseId, status, audible, transcript } = detail; + if (DEBUG) console.debug(`[ui] response-finished resp=${responseId} status=${status} audible=${audible} known=${this._asstByResp.has(responseId)}`); + // Without an id we can't target a specific response; the bubble will + // auto-dismiss on its own timer regardless. + if (!responseId) return; + const entry = this._asstByResp.get(responseId); + + if (status === "cancelled") { + // Keep every transcript that was received — mark it interrupted rather + // than erasing it. If the `*.transcript.done` never fired, build the row + // from the text carried in response.done. + let hist = entry?.hist ?? null; + if (!hist && transcript) { + hist = this._appendHistMsg("assistant", transcript, false); + } else if (hist && transcript) { + this._updateHistMsg(hist, transcript, false); + } + if (hist) this._markHistInterrupted(hist); + this._asstByResp.delete(responseId); + return; + } + + // Any other terminal close (completed / failed / incomplete / …): just + // release the map entry. The bubble already auto-dismisses on its timer and + // the history row persists as the conversation log. Crucially we do NOT + // touch user state here — that lifecycle is fully independent. + this._asstByResp.delete(responseId); + } + + /** The model called a tool — show an ephemeral "running" bubble. + * @param {string} name */ + onToolCall(name) { + this._bumpDismiss(this._spawnBubble("tool", name)); + this._markUnread(); + } + + /** The tool finished — append its call+result row (and any captured image). + * @param {string} name @param {string} argsJson @param {string} output @param {string} [image] */ + onToolResult(name, argsJson, output, image) { + this._appendHistTool(name, argsJson, output); + if (image) this._appendHistImage(image); // show the captured frame below the call + this._markUnread(); + } +} diff --git a/backend/vendor/s2s-cloud/demo/ui/dom.js b/backend/vendor/s2s-cloud/demo/ui/dom.js new file mode 100644 index 0000000..d5a645a --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/ui/dom.js @@ -0,0 +1,37 @@ +// @ts-check +/** Small shared helpers used across the UI modules: a strict query selector, + * HTML escaping for text we drop into innerHTML, error-string trimming, and + * the opt-in debug flag. */ + +/** Opt-in tracing: `localStorage.setItem("s2s.debug", "1")` then reload. */ +export const DEBUG = (() => { + try { + return localStorage.getItem("s2s.debug") === "1"; + } catch { + return false; + } +})(); + +/** + * Query a single element, throwing if it's missing (so a broken selector fails + * loudly at startup rather than as a later null-deref). + * @template {HTMLElement} T + * @param {string} selector + * @returns {T} + */ +export function $(selector) { + const el = document.querySelector(selector); + if (!el) throw new Error(`Missing element: ${selector}`); + return /** @type {T} */ (el); +} + +/** @param {string} s @returns {string} */ +export function escHtml(s) { + return s.replace(/&/g, "&").replace(//g, ">"); +} + +/** Trim a long error message to fit the orb caption. @param {string} text */ +export function truncateError(text) { + if (text.length <= 90) return text; + return text.slice(0, 87) + "…"; +} diff --git a/backend/vendor/s2s-cloud/demo/worklets/audio-playback.js b/backend/vendor/s2s-cloud/demo/worklets/audio-playback.js new file mode 100644 index 0000000..469b904 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/worklets/audio-playback.js @@ -0,0 +1,211 @@ +// @ts-check +/** + * AudioWorkletProcessor that plays back Float32 mono samples received from + * the main thread, upsampling whatever incoming rate the server uses + * (typically 16 kHz PCM16) to the AudioContext rate (typically 48 kHz). + * + * Lifecycle / messaging: + * + * main -> worklet: + * { kind: "config", inputRate: 16000, muted: false } one-shot at startup + * { kind: "audio", samples: Float32Array } per chunk + * { kind: "clear" } wipe queue (barge-in) + * + * worklet -> main: + * { kind: "stats", queuedMs, played } every ~250 ms + * { kind: "underrun" } every time the queue + * runs dry mid-playback + * + * `muted` (default false): the Live Avatar (LiveTalking WebRTC) carries the + * same audio, so the page mutes its own playback to avoid double audio. When + * the avatar is NOT running (e.g. dev on macOS), keep muted=false so the page + * plays the backend audio itself. + * + * IMPORTANT: the read position only advances while audio is actually being + * played (`_playing`). Advancing during idle would drift `_readIdx` far past + * the next chunk's length and turn every read into NaN (silence) once audio + * arrives. + */ + +const STATS_INTERVAL_FRAMES = 12000; +const FADE_FRAMES = 32; + +class AudioPlaybackProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this._inputRate = 16000; + this._stepRatio = this._inputRate / sampleRate; + this._muted = false; + this._queue = []; + this._readIdx = 0; + this._fracPos = 0; + this._playing = false; + this._lastSample = 0; + this._framesSinceStats = 0; + this._totalPlayed = 0; + this._dbgReceived = 0; + this._dbgOutPeak = 0; + this._fadeIn = 0; + this._fadeOut = 0; + + this.port.onmessage = (e) => { + const data = e.data; + if (!data || typeof data !== "object") return; + switch (data.kind) { + case "config": + if (typeof data.inputRate === "number" && data.inputRate > 0) { + this._inputRate = data.inputRate; + this._stepRatio = this._inputRate / sampleRate; + } + if (typeof data.muted === "boolean") this._muted = data.muted; + break; + case "audio": { + // Accept Float32Array, a transferred ArrayBuffer, or a plain + // {buffer, byteOffset, length} descriptor: cross-realm structured + // cloning can deliver the samples in any of these shapes. + let arr = data.samples; + let f32 = null; + if (arr instanceof Float32Array) { + f32 = arr; + } else if (arr instanceof ArrayBuffer) { + f32 = new Float32Array(arr); + } else if (arr && typeof arr === "object" && arr.buffer instanceof ArrayBuffer) { + f32 = new Float32Array(arr.buffer, arr.byteOffset || 0, (arr.byteLength || arr.buffer.byteLength) >> 2); + } + if (f32 && f32.length > 0) { + this._queue.push(f32); + this._dbgReceived += 1; + if (!this._playing) { + this._playing = true; + this._fadeIn = FADE_FRAMES; + this._fadeOut = 0; + } + } + break; + } + case "clear": + this._queue.length = 0; + this._readIdx = 0; + this._fracPos = 0; + this._playing = false; + this._lastSample = 0; + this._fadeOut = FADE_FRAMES; + break; + } + }; + } + + _queuedSamples() { + let total = -this._readIdx; + for (const buf of this._queue) total += buf.length; + return Math.max(0, total); + } + + /** Linear-interp read at the current fractional position. */ + _readInterpolated() { + if (this._queue.length === 0) return null; + const head = this._queue[0]; + const idx = this._readIdx; + const frac = this._fracPos; + + let a = head[idx]; + let b; + if (idx + 1 < head.length) { + b = head[idx + 1]; + } else if (this._queue.length > 1) { + b = this._queue[1][0]; + } else { + b = a; + } + return a + (b - a) * frac; + } + + /** Advance the read position by `stepRatio`; pop consumed buffers. */ + _advance() { + this._fracPos += this._stepRatio; + while (this._fracPos >= 1) { + this._fracPos -= 1; + this._readIdx += 1; + } + while (this._queue.length > 0 && this._readIdx >= this._queue[0].length) { + this._readIdx -= this._queue[0].length; + this._queue.shift(); + } + } + + process(_, outputs) { + const channels = outputs[0]; + if (!channels || channels.length === 0) return true; + const out = channels[0]; + const stereo = channels.length > 1 ? channels[1] : null; + + // Muted mode (Live Avatar WebRTC carries the audio): output silence. + if (this._muted) { + for (let i = 0; i < out.length; i++) { + out[i] = 0; + if (stereo) stereo[i] = 0; + } + this._framesSinceStats += out.length; + return true; + } + + for (let i = 0; i < out.length; i++) { + let sample = 0; + + if (this._playing) { + const v = this._readInterpolated(); + if (v === null) { + // Underrun: ramp out cleanly to avoid clicks. + sample = this._lastSample * Math.max(0, 1 - 1 / FADE_FRAMES); + this._lastSample = sample; + if (Math.abs(sample) < 1e-4) { + this._playing = false; + this._lastSample = 0; + this.port.postMessage({ kind: "underrun" }); + } + } else { + sample = v; + this._lastSample = v; + this._advance(); + } + + if (this._fadeIn > 0) { + const gain = 1 - this._fadeIn / FADE_FRAMES; + sample *= gain; + this._fadeIn -= 1; + } + if (this._fadeOut > 0) { + const gain = this._fadeOut / FADE_FRAMES; + sample *= gain; + this._fadeOut -= 1; + if (this._fadeOut === 0) { + this._playing = false; + this._lastSample = 0; + } + } + + this._totalPlayed += 1; + const abs = sample < 0 ? -sample : sample; + if (abs > this._dbgOutPeak) this._dbgOutPeak = abs; + } + + out[i] = sample; + if (stereo) stereo[i] = sample; + } + + this._framesSinceStats += out.length; + if (this._framesSinceStats >= STATS_INTERVAL_FRAMES) { + this.port.postMessage({ + kind: "stats", + queuedMs: Math.round((this._queuedSamples() / this._inputRate) * 1000), + played: this._totalPlayed, + received: this._dbgReceived, + outPeak: this._dbgOutPeak, + }); + this._framesSinceStats = 0; + } + return true; + } +} + +registerProcessor("audio-playback", AudioPlaybackProcessor); diff --git a/backend/vendor/s2s-cloud/demo/worklets/mic-capture.js b/backend/vendor/s2s-cloud/demo/worklets/mic-capture.js new file mode 100644 index 0000000..a48860b --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/worklets/mic-capture.js @@ -0,0 +1,159 @@ +// @ts-check +/** + * AudioWorkletProcessor that resamples the AudioContext rate (typically 48 kHz) + * down to 16 kHz, packs the result as little-endian Int16 PCM, and posts it + * back to the main thread in fixed-size chunks. + * + * The Hugging Face speech-to-speech WebSocket route expects the + * `input_audio_buffer.append` payload at 16 kHz PCM16 mono. + * + * Design notes: + * - 48 -> 16 is an exact 3:1 ratio so we use a 3-tap boxcar average as a + * cheap low-pass before decimating. Good enough for voice STT; we lose + * a tiny bit of >8 kHz content which the pipeline discards anyway. + * - Output frames are emitted at the cadence dictated by `chunkMs` + * (default 40 ms = 640 samples = 1280 bytes). The OpenAI Realtime + * server batches incoming audio so the cadence is flexible; 20-100 ms + * is the sweet spot. + * - Float -> Int16 saturates to [-1, 1] before scaling. + * - Optional noise gate: per-chunk RMS decides open/closed against a + * threshold; the gain ramps (fast attack, hold, slow release) so word + * onsets aren't clipped and quiet tails don't click. The gate only + * affects the audio we SEND; the main-thread visualiser taps the raw + * mic separately. We post the chunk RMS up every frame so the Settings + * mic meter can show the live level against the threshold. + */ + +const TARGET_RATE = 16000; +const DEFAULT_CHUNK_MS = 40; +// Gate envelope timing (fixed; only the threshold is user-tunable). +const GATE_ATTACK_MS = 5; // open almost instantly so word onsets survive +const GATE_HOLD_MS = 250; // stay open this long after the level drops back under +const GATE_RELEASE_MS = 80; // then fade closed over this long (no click) + +class MicCaptureProcessor extends AudioWorkletProcessor { + constructor(options) { + super(); + const chunkMs = options?.processorOptions?.chunkMs ?? DEFAULT_CHUNK_MS; + this._inputRate = sampleRate; + this._ratio = this._inputRate / TARGET_RATE; + this._chunkSamples16k = Math.round((TARGET_RATE * chunkMs) / 1000); + this._scratch = new Float32Array(0); + this._decimated = new Float32Array(this._chunkSamples16k); + this._enabled = true; + + // Noise gate state. Disabled by default (pure passthrough). + this._gateEnabled = false; + this._thresholdLin = 0; // linear amplitude; signal RMS must exceed this to open + this._gateGain = 1; // smoothed gain currently applied + this._holdRemaining = 0; // samples left before the gate may start closing + this._attackCoef = Math.exp(-1 / ((GATE_ATTACK_MS / 1000) * TARGET_RATE)); + this._releaseCoef = Math.exp(-1 / ((GATE_RELEASE_MS / 1000) * TARGET_RATE)); + this._holdSamples = Math.round((GATE_HOLD_MS / 1000) * TARGET_RATE); + + this.port.onmessage = (e) => { + const data = e.data; + if (data?.kind === "enable") this._enabled = !!data.value; + else if (data?.kind === "gate") { + this._gateEnabled = !!data.enabled; + // dB -> linear amplitude. When off, threshold 0 keeps the gate open. + this._thresholdLin = data.enabled ? Math.pow(10, data.thresholdDb / 20) : 0; + } + }; + } + + /** + * Append `incoming` to the internal scratch buffer, then emit as many + * full output chunks as we have material for. + * @param {Float32Array} incoming + */ + _ingest(incoming) { + if (incoming.length === 0) return; + const next = new Float32Array(this._scratch.length + incoming.length); + next.set(this._scratch, 0); + next.set(incoming, this._scratch.length); + this._scratch = next; + this._maybeEmit(); + } + + _maybeEmit() { + const r = this._ratio; + const n = this._chunkSamples16k; + const needIn = Math.ceil(n * r); + const dec = this._decimated; + while (this._scratch.length >= needIn) { + // 1. Decimate to 16 kHz floats and accumulate energy for the gate/meter. + let sumSq = 0; + if (Math.abs(r - 3) < 1e-6) { + // 48 kHz -> 16 kHz fast path with boxcar lowpass. + for (let i = 0; i < n; i++) { + const idx = i * 3; + const s = (this._scratch[idx] + this._scratch[idx + 1] + this._scratch[idx + 2]) / 3; + dec[i] = s; + sumSq += s * s; + } + } else { + // Generic path: linear interpolation. Slower but works at any rate + // (e.g. some Windows boxes report sampleRate=44100). + for (let i = 0; i < n; i++) { + const srcPos = i * r; + const idx = Math.floor(srcPos); + const frac = srcPos - idx; + const a = this._scratch[idx]; + const b = this._scratch[idx + 1] ?? a; + const s = a + (b - a) * frac; + dec[i] = s; + sumSq += s * s; + } + } + const rms = Math.sqrt(sumSq / n); + + // 2. Decide the gate target for this chunk, then ramp sample-by-sample. + let target = 1; + if (this._gateEnabled) { + if (rms >= this._thresholdLin) { + this._holdRemaining = this._holdSamples; // re-arm the hold + } else if (this._holdRemaining > 0) { + this._holdRemaining -= n; // coasting through the hold window + } else { + target = 0; + } + } + + // 3. Apply the (smoothed) gain and pack to Int16. + const out = new Int16Array(n); + let gain = this._gateGain; + for (let i = 0; i < n; i++) { + const coef = target > gain ? this._attackCoef : this._releaseCoef; + gain = target + (gain - target) * coef; + const s = dec[i] * gain; + const clamped = s < -1 ? -1 : s > 1 ? 1 : s; + out[i] = clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff; + } + this._gateGain = gain; + + // Shift the scratch buffer to keep only the trailing unused samples. + const consumed = Math.floor(n * r); + this._scratch = this._scratch.slice(consumed); + + // Live input level for the Settings meter (raw RMS, pre-gate). + this.port.postMessage({ kind: "level", rms }); + + if (this._enabled) { + this.port.postMessage(out.buffer, [out.buffer]); + } + // When disabled (mic muted) we silently consume input so the worklet + // stays alive and the buffer never grows unbounded. + } + } + + process(inputs) { + const input = inputs[0]; + if (!input || input.length === 0 || !input[0]) return true; + const mono = input[0]; + if (mono.length > 0) this._ingest(mono); + return true; + } +} + +registerProcessor("mic-capture", MicCaptureProcessor); diff --git a/backend/vendor/s2s-cloud/demo/worklettest.html b/backend/vendor/s2s-cloud/demo/worklettest.html new file mode 100644 index 0000000..af05876 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/worklettest.html @@ -0,0 +1,55 @@ + + +worklet 输出测试 + +
running...
+ + + diff --git a/backend/vendor/s2s-cloud/demo/ws/codec.js b/backend/vendor/s2s-cloud/demo/ws/codec.js new file mode 100644 index 0000000..511ad9e --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/ws/codec.js @@ -0,0 +1,57 @@ +// @ts-check +/** + * Pure, stateless helpers for the WebSocket realtime client: base64 <-> PCM + * conversion for the audio frames on the wire, transcript extraction from a + * `response.done` payload, and a tiny URL helper. Kept separate from the client + * so the protocol/state logic stays readable. + */ + +/** @param {string} url */ +export function trimTrailingSlash(url) { + return url.endsWith("/") ? url.slice(0, -1) : url; +} + +/** + * Pull the assistant transcript out of a `response.done` payload. The text + * lives in `response.output[].content[].transcript` (audio) or `.text`. Used as + * the source of truth for interrupted replies, where the dedicated + * `*.transcript.done` event may never arrive. + * @param {any} response + * @returns {string} + */ +export function extractResponseTranscript(response) { + const output = response?.output; + if (!Array.isArray(output)) return ""; + /** @type {string[]} */ + const parts = []; + for (const item of output) { + for (const part of item?.content ?? []) { + const text = part?.transcript ?? part?.text; + if (typeof text === "string" && text.trim()) parts.push(text.trim()); + } + } + return parts.join(" ").trim(); +} + +/** @param {ArrayBuffer} buf */ +export function base64FromArrayBuffer(buf) { + const bytes = new Uint8Array(buf); + // Chunked encoding so we don't blow up the call stack on long buffers. + let binary = ""; + const chunk = 0x8000; + for (let i = 0; i < bytes.length; i += chunk) { + binary += String.fromCharCode.apply(null, /** @type {number[]} */ ( + /** @type {unknown} */ (bytes.subarray(i, i + chunk)) + )); + } + return btoa(binary); +} + +/** @param {string} b64 */ +export function base64ToBytes(b64) { + const binary = atob(b64); + const len = binary.length; + const out = new Uint8Array(len); + for (let i = 0; i < len; i++) out[i] = binary.charCodeAt(i); + return out; +} diff --git a/backend/vendor/s2s-cloud/demo/ws/orb-visualizer.js b/backend/vendor/s2s-cloud/demo/ws/orb-visualizer.js new file mode 100644 index 0000000..082f601 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/ws/orb-visualizer.js @@ -0,0 +1,98 @@ +// @ts-check +/** + * Orb spectrum visualiser. Each animation frame it reads two AnalyserNodes (the + * mic input and the TTS output) and maps the low-frequency speech energy onto + * the orb's CSS custom properties: + * - `--bar0`..`--bar4` the 5-band level meter + * - `--ai-audio-level` the global "Reachy talks" glow / scale pulse + * + * The bottom of the FFT is where speech energy lives, so the band edges stay + * low — that keeps the bars dancing on voice rather than on noise. While the AI + * is speaking we source the bars from the OUTPUT analyser so the orb pulses with + * Reachy's voice instead of sitting dead while the user is silent. + */ + +// Exported so the client can size its AnalyserNodes to match our buffer. +export const VIS_FFT_SIZE = 256; +const VIS_BAND_COUNT = 5; +const VIS_BAND_EDGES = [2, 5, 9, 16, 28, 52]; +const VIS_ATTACK = 0.6; // weight for new sample on upswing (snappy) +const VIS_RELEASE = 0.18; // weight for new sample on decay (gentle fade) + +export class OrbVisualiser { + /** + * @param {AnalyserNode} micAnalyser + * @param {AnalyserNode} outAnalyser + * @param {() => boolean} isAiSpeaking Source the bars from the AI output when + * true, otherwise from the mic. + */ + constructor(micAnalyser, outAnalyser, isAiSpeaking) { + this._mic = micAnalyser; + this._out = outAnalyser; + this._isAiSpeaking = isAiSpeaking; + this._buf = new Uint8Array(micAnalyser.frequencyBinCount); + this._bands = new Float32Array(VIS_BAND_COUNT); + this._aiLevel = 0; + /** @type {number | null} */ + this._frame = null; + } + + /** Begin the rAF loop (idempotent). */ + start() { + if (this._frame !== null) return; + const root = document.documentElement; + const tick = () => { + this._frame = requestAnimationFrame(tick); + this._update(root); + }; + this._frame = requestAnimationFrame(tick); + } + + /** Stop the loop and clear the CSS vars so the orb returns to rest. */ + stop() { + if (this._frame !== null) { + cancelAnimationFrame(this._frame); + this._frame = null; + } + const root = document.documentElement; + for (let i = 0; i < VIS_BAND_COUNT; i++) root.style.removeProperty(`--bar${i}`); + root.style.removeProperty("--ai-audio-level"); + } + + /** @param {HTMLElement} root */ + _update(root) { + // Mic bars: split FFT into 5 log-ish bands, smooth, write CSS vars. + const source = this._isAiSpeaking() ? this._out : this._mic; + source.getByteFrequencyData(this._buf); + + for (let b = 0; b < VIS_BAND_COUNT; b++) { + const lo = VIS_BAND_EDGES[b]; + const hi = VIS_BAND_EDGES[b + 1]; + let sum = 0; + let n = 0; + for (let i = lo; i < hi && i < this._buf.length; i++) { + sum += this._buf[i]; + n += 1; + } + const target = n > 0 ? sum / (n * 255) : 0; + const prev = this._bands[b]; + const k = target > prev ? VIS_ATTACK : VIS_RELEASE; + const next = prev + (target - prev) * k; + this._bands[b] = next; + root.style.setProperty(`--bar${b}`, next.toFixed(3)); + } + + // Global AI audio level: peak of the output analyser, used by the CSS to + // make the orb's glow / scale react to Reachy's voice. + this._out.getByteFrequencyData(this._buf); + let peak = 0; + const limit = Math.min(this._buf.length, VIS_BAND_EDGES[VIS_BAND_COUNT]); + for (let i = 0; i < limit; i++) { + if (this._buf[i] > peak) peak = this._buf[i]; + } + const aiTarget = peak / 255; + const k = aiTarget > this._aiLevel ? VIS_ATTACK : VIS_RELEASE; + this._aiLevel = this._aiLevel + (aiTarget - this._aiLevel) * k; + root.style.setProperty("--ai-audio-level", this._aiLevel.toFixed(3)); + } +} diff --git a/backend/vendor/s2s-cloud/demo/ws/s2s-ws-client.js b/backend/vendor/s2s-cloud/demo/ws/s2s-ws-client.js new file mode 100644 index 0000000..1843c72 --- /dev/null +++ b/backend/vendor/s2s-cloud/demo/ws/s2s-ws-client.js @@ -0,0 +1,1176 @@ +// @ts-check +/** + * Minimal WebSocket client for the Hugging Face speech-to-speech load balancer. + * + * Two-step handshake (same /session route as the WebRTC client): + * + * 1. POST `/session` -> JSON `{ connect_url: wss:///v1/realtime?session_token=, ... }` + * 2. Open a WebSocket directly on `connect_url` (no rewrite, unlike the WebRTC client). + * + * Once the socket is open we follow the OpenAI Realtime GA WebSocket + * protocol: + * + * - Server pushes `session.created` immediately after upgrade. + * - We send `session.update` (GA schema: `session.audio.{input,output}`, + * `session.output_modalities`, ...). + * - We stream mic audio as PCM16 16 kHz mono base64 chunks via + * `input_audio_buffer.append`. + * - The server pushes `response.output_audio.delta` (PCM16 24 kHz mono + * base64) and transcript deltas. + * + * Audio is handled internally via two AudioWorklet processors so the + * client owns the full mic-in / speaker-out pipeline. The main app only + * sees high-level lifecycle events (`status`, `transcript`, `error`, + * `session`), the same shape as the WebRTC client. + * + * @typedef {"idle" | "creating-session" | "queued" | "your-turn" | "connecting" | + * "connected" | "user-speaking" | "processing" | "ai-speaking" | + * "closed" | "error" + * } WsStatus + * + * @typedef {Object} WsSessionInfo + * @property {string} sessionId + * @property {string} connectUrl + * @property {string} websocketUrl + * @property {string} sessionToken + * @property {number} pendingTimeoutS + * @property {string} [tier] Login tier from the session proxy ("anon"|"free"|"pro"). + * @property {boolean} [limited] Whether this session is metered (heartbeat needed). + * @property {number} [heartbeatSec] Suggested heartbeat cadence in seconds. + * @property {number} [remainingSec] Daily budget left after this grant (display). + * + * @typedef {Object} WsClientOptions + * @property {string} [sessionUrl] URL to POST for the session handshake (returns + * `{ connect_url, ... }`). Usually a same-origin proxy like `api/session` so the + * load-balancer address stays server-side. Provide this OR `directUrl`. + * @property {string} [loadBalancerUrl] Load-balancer base URL. Legacy/direct + * alternative to `sessionUrl`: the client POSTs `/session` itself. Prefer + * `sessionUrl` so the LB address isn't exposed to the browser. + * @property {string} [directUrl] Full WebSocket URL of an s2s realtime endpoint + * (e.g. `ws://localhost:8080/v1/realtime`). When set, the client skips the + * session POST and dials it directly — no load balancer in between. + * @property {string} voice + * @property {string} instructions + * @property {MediaStream} [micStream] Live mic stream. Provide this OR `acquireMic`. + * @property {() => Promise} [acquireMic] Lazily obtain the mic stream, + * called only once a session is actually granted (after any queue wait). Lets the + * caller prime mic permission up front but not hold the mic 'in use' indicator on + * while waiting in line. Ignored if `micStream` is already set. + * @property {AudioContext} [audioContext] Pre-created (and resumed) context. + * iOS Safari only lets an AudioContext start from within a user gesture, so + * the caller creates/resumes it synchronously on the orb tap and hands it + * here; otherwise it stays suspended (silent) after the mic/session awaits. + * @property {ToolDef[]} [tools] Function tools declared to the backend in the + * initial `session.update`. The model decides when to call them; the caller + * executes and replies via `sendToolOutput` + `requestResponse`. + * @property {NoiseGate} [noiseGate] Client-side noise gate applied to the mic + * before it's sent. Tunable live via `setNoiseGate`. + * + * @typedef {Object} NoiseGate + * @property {boolean} enabled + * @property {number} thresholdDb Open threshold in dBFS (e.g. -45). + * + * @typedef {Object} ToolDef + * @property {"function"} type + * @property {string} name + * @property {string} description + * @property {object} parameters JSON Schema for the call arguments. + * + * @typedef {Object} TranscriptEvent + * @property {"user" | "assistant"} role + * @property {string} text + * @property {boolean} partial + */ + +import { + base64FromArrayBuffer, + base64ToBytes, + extractResponseTranscript, + trimTrailingSlash, +} from "./codec.js"; +import { OrbVisualiser, VIS_FFT_SIZE } from "./orb-visualizer.js"; + +/** Build an Error carrying a `code` (and optional extra fields) so callers can + * branch on the failure kind: "limit" | "queue-full" | "queue-expired" | "aborted". + * @param {string} message @param {string} code @param {object} [extra] */ +function _codedError(message, code, extra) { + const err = /** @type {Error & { code?: string }} */ (new Error(message)); + err.code = code; + if (extra) Object.assign(err, extra); + return err; +} + +// The s2s pipeline runs internally at 16 kHz mono PCM. The WebRTC transport +// resamples to 48 kHz for Opus, but the WebSocket transport emits the +// native pipeline rate. We don't (can't) override it via `audio.output.format` +// because the server's pydantic validator rejects the whole `session.update` +// as soon as a sub-field shape it doesn't know about appears. +const OUTPUT_SAMPLE_RATE = 16000; +const MIC_CHUNK_MS = 40; + +export class S2sWsRealtimeClient extends EventTarget { + /** @param {WsClientOptions} options */ + constructor(options) { + super(); + /** @type {WsClientOptions} */ + this.options = options; + /** Audio debug counters (see getAudioDebug). */ + this._dbgDeltas = 0; + this._dbgBytes = 0; + this._dbgPlayed = 0; + /** @type {ToolDef[]} Function tools declared to the backend. */ + this._tools = options.tools ?? []; + /** @type {string} Direct realtime WS URL (set => skip the LB session POST). */ + this._directUrl = options.directUrl ?? ""; + /** @type {string} Where to POST for the session handshake. Prefer the + * explicit `sessionUrl`; fall back to `/session` for callers + * that still pass the LB address directly. */ + this._sessionUrl = options.sessionUrl + ? options.sessionUrl + : options.loadBalancerUrl + ? `${trimTrailingSlash(options.loadBalancerUrl)}/session` + : ""; + /** @type {(() => Promise) | null} Lazy mic acquisition (post-grant). */ + this._acquireMic = options.acquireMic ?? null; + /** @type {boolean} Set by close() to abort a queue wait in progress. */ + this._closed = false; + /** @type {string} The active queue ticket id while waiting (else ""). */ + this._queueId = ""; + /** @type {(() => void) | null} Wakes the queue poll sleep early on close(). */ + this._queueWake = null; + /** @type {ReturnType | 0} */ + this._queueTimer = 0; + // Join gate: after waiting in line the caller must explicitly `join()` before + // we dial, so a slot isn't spent on someone who walked away. Resolved by + // join(), rejected on timeout (the LB reclaims the slot) or close(). + /** @type {(() => void) | null} */ + this._joinResolve = null; + /** @type {((err: Error) => void) | null} */ + this._joinReject = null; + /** @type {ReturnType | 0} */ + this._joinTimer = 0; + /** @type {NoiseGate} Mic noise gate; off by default. */ + this._noiseGate = options.noiseGate ?? { enabled: false, thresholdDb: -45 }; + /** @type {WebSocket | null} */ + this._ws = null; + /** @type {AudioContext | null} */ + this._ctx = null; + /** @type {MediaStreamAudioSourceNode | null} */ + this._micSrc = null; + /** @type {AudioWorkletNode | null} */ + this._captureNode = null; + /** @type {AudioWorkletNode | null} */ + this._playbackNode = null; + /** @type {GainNode | null} */ + this._captureSink = null; + /** @type {AnalyserNode | null} */ + this._micAnalyser = null; + /** @type {AnalyserNode | null} */ + this._outAnalyser = null; + /** @type {OrbVisualiser | null} */ + this._visualiser = null; + /** @type {WsStatus} */ + this._status = "idle"; + this._aiSpeaking = false; + /** @type {Set} response_ids that have actually played audio, so the + * UI can tell a barge-in cut (keep it) from a never-heard speculative + * response (drop it). */ + this._audibleResponses = new Set(); + /** @type {Map} The CURRENT assistant transcript segment per + * response, accumulated from streamed deltas (reset on each segment's done). */ + this._asstTranscriptByResp = new Map(); + /** @type {Map} Completed assistant transcript segments per + * response, space-joined. A single response can emit several + * `*.transcript.done` events; we concatenate them until response.done. */ + this._asstFullByResp = new Map(); + this._muted = false; + // ── Response lock ──────────────────────────────────────────────────── + // The backend allows only ONE response in flight: creating a second while + // one is active fails with `conversation_already_has_active_response`. So + // we serialize response.create. `_openResponses` counts responses the + // server has confirmed (response.created) but not yet finished + // (response.done) — it's cumulative, so every create maps to one done. + // `_createInFlight` covers the window after we send a create but before its + // response.created echo. Any requestResponse() made while locked is queued + // and replayed, one at a time, as each response.done frees the slot. + this._openResponses = 0; + this._createInFlight = false; + /** @type {{ image?: string }[]} Pending response.create payloads, one per + * queued requestResponse(). A payload may carry an image to send just + * before its create (so the frame travels with the create, not eagerly). */ + this._createQueue = []; + /** @type {Promise | null} */ + this._readyPromise = null; + this._sessionConfigured = false; + this._debug = (() => { try { return localStorage.getItem("s2s.debug") === "1"; } catch { return false; } })(); + } + + get status() { + return this._status; + } + + /** @param {WsStatus} status */ + _setStatus(status) { + if (this._status === status) return; + this._status = status; + this.dispatchEvent(new CustomEvent("status", { detail: { status } })); + } + + /** Full assistant transcript so far for a response: the completed segments + * plus the in-progress one, all space-joined. + * @param {string} rid @returns {string} */ + _asstDisplay(rid) { + const full = this._asstFullByResp.get(rid) || ""; + const seg = this._asstTranscriptByResp.get(rid) || ""; + if (!seg) return full; + return full ? `${full} ${seg}` : seg; + } + + _markAudible() { + if (this._status === "ai-speaking") return; + if (this._status === "closed" || this._status === "error") return; + this._setStatus("ai-speaking"); + } + + /** + * Full handshake. Resolves once the WS is open AND the audio pipeline is + * ready to send/receive samples. + * @returns {Promise} + */ + async connect() { + if (this._ws) throw new Error("Already connected"); + + let connectUrl; + if (this._directUrl) { + // Direct mode: no load balancer, no /session POST — dial the realtime + // endpoint straight away (e.g. a local s2s server). + connectUrl = this._directUrl; + this._setStatus("connecting"); + } else { + if (!this._sessionUrl) { + throw new Error("No session endpoint or direct URL configured"); + } + this._setStatus("creating-session"); + const { grant, waited } = await this._createSessionOrQueue(); + if (this._closed) throw _codedError("connect aborted", "aborted"); + // If we waited in line, don't dial until the user explicitly joins — this + // keeps a freed slot from being spent on someone who stepped away, and the + // click is a fresh gesture (re-arms the AudioContext on iOS). + if (waited) { + await this._awaitJoin(grant); + if (this._closed) throw _codedError("connect aborted", "aborted"); + } + this.dispatchEvent(new CustomEvent("session", { detail: { info: grant } })); + connectUrl = grant.connectUrl; + this._setStatus("connecting"); + } + + // Acquire the mic now — only once a slot is actually ours. The caller primed + // permission up front, so this is silent and the 'in use' indicator lights + // only for a real, connecting session (never during a queue wait). + if (!this.options.micStream && this._acquireMic) { + this.options.micStream = await this._acquireMic(); + } + + // Spin up the AudioContext + worklets in parallel with the WS dial. + const audioReady = this._setupAudio(); + const wsReady = this._openWebSocket(connectUrl); + await Promise.all([audioReady, wsReady]); + } + + /** + * POST the session handshake; if the pool is busy, wait in the queue (polling + * position) until a slot is claimed. Resolves to a grant plus whether we had to + * wait (which decides if an explicit join is required before dialing). + * @returns {Promise<{ grant: WsSessionInfo, waited: boolean }>} + */ + async _createSessionOrQueue() { + const first = await this._postSession(); + if (first.state === "queued") { + this._setStatus("queued"); + const grant = await this._pollQueue(first); + return { grant, waited: true }; + } + return { grant: first.grant, waited: false }; + } + + /** + * Hold at the front of the line until the user clicks join (resolves the gate) + * or the grant lapses. Announces "your-turn" + a deadline the UI counts down. + * @param {WsSessionInfo} grant + * @returns {Promise} + */ + _awaitJoin(grant) { + // The LB reclaims an unclaimed slot at its pending timeout; expire the gate a + // touch earlier so we never dial a session the LB just reaped. + const windowS = Math.max(3, (grant.pendingTimeoutS || 60) - 3); + this._setStatus("your-turn"); + this.dispatchEvent( + new CustomEvent("ready-to-join", { detail: { info: grant, expiresSec: windowS } }), + ); + return new Promise((resolve, reject) => { + this._joinResolve = resolve; + this._joinReject = reject; + this._joinTimer = setTimeout(() => { + this._joinResolve = null; + this._joinReject = null; + reject(_codedError("Your spot expired", "join-expired")); + }, windowS * 1000); + }); + } + + /** Accept the held slot and let connect() proceed to dial. Called from the + * "Join now" click, so it's a user gesture: re-resume the AudioContext, which + * iOS may have suspended while we waited. */ + join() { + if (this._joinTimer) { + clearTimeout(this._joinTimer); + this._joinTimer = 0; + } + try { + void this.options.audioContext?.resume(); + } catch { + // best-effort; _setupAudio resumes again + } + const resolve = this._joinResolve; + this._joinResolve = null; + this._joinReject = null; + resolve?.(); + } + + /** + * POST /session once. Returns either a granted session or a queue ticket. + * @returns {Promise<{ state: "granted", grant: WsSessionInfo } | { state: "queued", queueId: string, position: number, pollIntervalS: number }>} + */ + async _postSession() { + const url = this._sessionUrl; + console.log("[ws] POST", url); + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }); + if (response.status === 402) { + // The session proxy refused: today's per-tier time budget is spent. Surface + // it as a typed error so the UI shows the limit modal, not a crash. + const body = await response.json().catch(() => ({})); + throw _codedError("Daily conversation limit reached", "limit", { tier: body?.tier }); + } + if (response.status === 503) { + const body = await response.json().catch(() => ({})); + if (body?.state === "at_capacity") { + throw _codedError("The queue is full — try again shortly.", "queue-full"); + } + throw new Error("/session failed (503)"); + } + if (!response.ok) { + const text = await response.text().catch(() => ""); + throw new Error(`/session failed (${response.status}): ${text}`); + } + const json = await response.json(); + if (json.state === "queued") { + return { + state: "queued", + queueId: json.queue_id, + position: json.position, + pollIntervalS: json.poll_interval_s, + }; + } + return { state: "granted", grant: this._parseGrant(json) }; + } + + /** + * Poll the waiting queue until this ticket claims a slot. Emits `queue` events + * ({ position }) as the line advances. Throws on limit (402), expiry (404), or + * close(). Transient network/5xx blips are ignored and retried next tick. + * @param {{ queueId: string, position: number, pollIntervalS: number }} ticket + * @returns {Promise} + */ + async _pollQueue(ticket) { + const intervalMs = Math.max(1, ticket.pollIntervalS || 2) * 1000; + this._queueId = ticket.queueId; + this._emitQueue(ticket.position); + + while (true) { + await this._queueSleep(intervalMs); + if (this._closed) throw _codedError("queue wait aborted", "aborted"); + + let response; + try { + response = await fetch(`api/queue/${encodeURIComponent(this._queueId)}`, { + headers: { "Content-Type": "application/json" }, + }); + } catch { + continue; // network blip — keep our place, retry next tick + } + + if (response.status === 402) { + const body = await response.json().catch(() => ({})); + throw _codedError("Daily conversation limit reached", "limit", { tier: body?.tier }); + } + if (response.status === 404) { + throw _codedError("Queue timed out", "queue-expired"); + } + if (!response.ok) continue; // 502/503 — transient, retry + + const json = await response.json().catch(() => null); + if (!json) continue; + if (json.state === "queued") { + this._emitQueue(json.position); + continue; + } + // Reached the front and claimed a slot. + this._queueId = ""; + return this._parseGrant(json); + } + } + + /** @param {number} position */ + _emitQueue(position) { + this.dispatchEvent( + new CustomEvent("queue", { detail: { position, queueId: this._queueId } }), + ); + } + + /** A sleep that close() can cut short so a queued client tears down promptly. + * @param {number} ms */ + _queueSleep(ms) { + return new Promise((resolve) => { + this._queueWake = resolve; + this._queueTimer = setTimeout(() => { + this._queueWake = null; + resolve(); + }, ms); + }); + } + + /** @param {any} json @returns {WsSessionInfo} */ + _parseGrant(json) { + return { + sessionId: json.session_id, + connectUrl: json.connect_url, + websocketUrl: json.websocket_url, + sessionToken: json.session_token, + pendingTimeoutS: json.pending_timeout_s, + tier: json.tier, + limited: json.limited, + heartbeatSec: json.heartbeatSec, + remainingSec: json.remainingSec, + }; + } + + async _setupAudio() { + // Prefer a context the caller already created + resumed inside the tap + // gesture (required on iOS). Fall back to creating one here for callers + // that don't (desktop is lenient about the gesture timing). + // Most desktops give us 48 kHz, mobiles can give 44.1/24/16 kHz; the + // capture worklet handles any rate (linear interp fallback). + const ctx = this.options.audioContext ?? new AudioContext({ latencyHint: "interactive" }); + this._ctx = ctx; + + // Resume if still suspended. This is best-effort here — on iOS the resume + // that actually counts is the one the caller did synchronously on tap. + if (ctx.state === "suspended") { + try { + await ctx.resume(); + } catch (err) { + console.warn("[ws] AudioContext resume failed:", err); + } + } + + // The worklets live at the repo root, one level up from this module. + // ?v=2 busts the browser cache so the fixed (unmuted) playback worklet + // always loads instead of a stale muted copy. + const base = new URL("../worklets/", import.meta.url); + await ctx.audioWorklet.addModule(new URL("mic-capture.js?v=3", base).href); + await ctx.audioWorklet.addModule(new URL("audio-playback.js?v=3", base).href); + + const captureNode = new AudioWorkletNode(ctx, "mic-capture", { + numberOfInputs: 1, + numberOfOutputs: 0, + processorOptions: { chunkMs: MIC_CHUNK_MS }, + }); + captureNode.port.onmessage = (e) => { + const data = e.data; + if (data instanceof ArrayBuffer) { + this._onMicChunk(data); + } else if (data?.kind === "level") { + // Raw pre-gate mic RMS for the Settings meter. + this.dispatchEvent(new CustomEvent("input-level", { detail: { rms: data.rms } })); + } + }; + // Push the initial gate config now that the worklet exists. + captureNode.port.postMessage({ kind: "gate", ...this._noiseGate }); + this._captureNode = captureNode; + + const micSrc = ctx.createMediaStreamSource(this.options.micStream); + micSrc.connect(captureNode); + this._micSrc = micSrc; + + // Mic analyser: tap the mic in parallel with the worklet so we get the + // raw (un-resampled, un-clipped) signal for the visualiser. + const micAnalyser = ctx.createAnalyser(); + micAnalyser.fftSize = VIS_FFT_SIZE; + micAnalyser.smoothingTimeConstant = 0; + micSrc.connect(micAnalyser); + this._micAnalyser = micAnalyser; + + const playbackNode = new AudioWorkletNode(ctx, "audio-playback", { + numberOfInputs: 0, + numberOfOutputs: 1, + outputChannelCount: [1], + }); + playbackNode.port.postMessage({ kind: "config", inputRate: OUTPUT_SAMPLE_RATE, muted: this._playbackMuted === true }); + playbackNode.port.onmessage = (e) => this._onPlaybackMessage(e.data); + + // Output analyser sits between the playback worklet and the speakers. + const outAnalyser = ctx.createAnalyser(); + outAnalyser.fftSize = VIS_FFT_SIZE; + outAnalyser.smoothingTimeConstant = 0.3; + playbackNode.connect(outAnalyser); + outAnalyser.connect(ctx.destination); + this._outAnalyser = outAnalyser; + this._playbackNode = playbackNode; + + this._visualiser = new OrbVisualiser(micAnalyser, outAnalyser, () => this._aiSpeaking); + this._visualiser.start(); + } + + /** @param {string} connectUrl */ + _openWebSocket(connectUrl) { + return new Promise((resolve, reject) => { + const ws = new WebSocket(connectUrl); + ws.binaryType = "arraybuffer"; + this._ws = ws; + + const onceOpen = () => { + ws.removeEventListener("open", onceOpen); + ws.removeEventListener("error", onceErr); + resolve(); + }; + const onceErr = (e) => { + ws.removeEventListener("open", onceOpen); + ws.removeEventListener("error", onceErr); + reject(new Error(`WebSocket failed to open: ${e?.type ?? "error"}`)); + }; + ws.addEventListener("open", onceOpen); + ws.addEventListener("error", onceErr); + + ws.addEventListener("message", (e) => this._onWsMessage(e.data)); + ws.addEventListener("close", (e) => this._onWsClose(e)); + ws.addEventListener("error", (e) => { + console.error("[ws] socket error", e); + }); + }); + } + + /** + * @param {{ kind: string; queuedMs?: number; played?: number }} data + */ + _onPlaybackMessage(data) { + if (data?.kind === "underrun") { + // Server stopped sending audio mid-response. Most likely the turn + // ended cleanly (a response.done usually arrives just before/after + // this). We let the state machine fall back to "connected" via the + // response.done event handler. + } else if (data?.kind === "stats") { + if (typeof data.played === "number") this._dbgPlayed = data.played; + if (typeof data.received === "number") this._dbgReceived = data.received; + if (typeof data.outPeak === "number") this._dbgOutPeak = data.outPeak; + } + } + + /** + * Audio pipeline debug readout for the status overlay. + * @returns {{ deltas: number, bytes: number, played: number, ctx: string, ws: number }} + */ + getAudioDebug() { + return { + deltas: this._dbgDeltas, + bytes: this._dbgBytes, + received: this._dbgReceived ?? 0, + played: this._dbgPlayed, + peak: this._dbgPeak ?? 0, + outPeak: this._dbgOutPeak ?? 0, + ctx: this._ctx ? this._ctx.state : "none", + ws: this._ws ? this._ws.readyState : -1, + }; + } + + /** + * Mic worklet just sent us a ~40 ms PCM16 16 kHz mono chunk. + * Base64-encode and forward via the WS. + * @param {ArrayBuffer} pcm16Buffer + */ + _onMicChunk(pcm16Buffer) { + if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return; + if (!this._sessionConfigured) return; // Server rejects audio before session.update. + if (this._muted) return; + const b64 = base64FromArrayBuffer(pcm16Buffer); + this._send({ type: "input_audio_buffer.append", audio: b64 }); + } + + /** + * @param {string | ArrayBuffer | Blob} raw + */ + async _onWsMessage(raw) { + let text; + if (typeof raw === "string") { + text = raw; + } else if (raw instanceof ArrayBuffer) { + text = new TextDecoder("utf-8").decode(raw); + } else if (raw instanceof Blob) { + text = await raw.text(); + } else { + return; + } + + let event; + try { + event = JSON.parse(text); + } catch { + return; + } + + const type = event?.type; + if (typeof type !== "string") return; + // Opt-in event tracing for diagnosing turn/transcript issues. Enable with + // `localStorage.setItem("s2s.debug", "1")` in the browser console. + if (this._debug) { + const extra = type.startsWith("conversation.item.input_audio_transcription") + ? ` item=${event.item_id} ci=${event.content_index} ${event.delta ?? event.transcript ?? ""}` + : type.startsWith("response.") + ? ` resp=${event.response_id ?? event.response?.id ?? ""} status=${event.response?.status ?? ""} ${event.transcript ?? ""}` + : ""; + console.debug(`[ws] ${type}${extra}`); + } + + switch (type) { + case "session.created": + // Server-side defaults for the s2s pipeline are already what we + // want (server_vad, whisper-1 transcription, PCM16 16k in / 24k + // out). We only push the user-tunable bits: voice + instructions. + this._sendSessionUpdate(); + this._sessionConfigured = true; + if (this._status === "connecting") this._setStatus("connected"); + break; + + case "session.updated": + // Acknowledged by server, nothing to do. + break; + + case "input_audio_buffer.speech_started": + // User started speaking — stop any audio still playing OR queued, every + // time. We clear unconditionally (not just when `_aiSpeaking`): after a + // reply or a tool result the worklet's ring buffer can still be draining + // even though we already flipped `_aiSpeaking` off, and that tail would + // otherwise keep playing over the user's barge-in. + this._playbackNode?.port.postMessage({ kind: "clear" }); + this._aiSpeaking = false; + this._setStatus("user-speaking"); + break; + + case "input_audio_buffer.speech_stopped": + if (this._status === "user-speaking") this._setStatus("processing"); + break; + + case "response.created": + // A response now owns the slot — count it and clear our create guard + // (this confirms either our create or a server-initiated one). + this._openResponses++; + this._createInFlight = false; + if (this._status === "connected" || this._status === "user-speaking") { + this._setStatus("processing"); + } + break; + + case "response.output_item.added": + if (this._status === "connected" || this._status === "user-speaking") { + this._setStatus("processing"); + } + break; + + case "response.audio.delta": + case "response.output_audio.delta": { + this._pushAudioDelta(event.delta); + const rid = event.response_id ?? event.response?.id; + if (rid) this._audibleResponses.add(rid); + if (!this._aiSpeaking) { + this._aiSpeaking = true; + this._markAudible(); + } + break; + } + + case "response.content_part.added": { + const part = event.part; + if (part?.type === "audio" || part?.type === "output_audio") { + this._markAudible(); + } + break; + } + + case "response.done": { + this._aiSpeaking = false; + // This response freed the slot (completion OR cancellation both arrive + // as response.done). Decrement and, if a create was waiting, replay it. + this._openResponses = Math.max(0, this._openResponses - 1); + if (this._status === "ai-speaking" || this._status === "processing") { + this._setStatus("connected"); + } + // A response closes here for BOTH normal completion and cancellation + // (the s2s server signals a speculative-turn interrupt as + // `response.done` with status "cancelled" — there is no separate + // `response.cancelled` event). Surface the id + status so the UI can + // drop a cancelled response's transcript and commit a completed one. + const status = event.response?.status ?? "completed"; + const responseId = event.response?.id ?? ""; + // Did this response ever play audio? Distinguishes a barge-in cut (the + // user heard part of it) from a speculative response that never played. + const audible = responseId ? this._audibleResponses.has(responseId) : false; + this._audibleResponses.delete(responseId); + // Pull whatever transcript the response carries, falling back to the + // segments we concatenated from the `*.transcript.done` events (plus any + // in-progress delta). For an interrupted reply the response payload may + // be empty, so this is the last chance to capture the text. + const transcript = + extractResponseTranscript(event.response) || + this._asstDisplay(responseId) || + ""; + // Response finished — clear both transcript accumulators for it. + this._asstTranscriptByResp.delete(responseId); + this._asstFullByResp.delete(responseId); + this.dispatchEvent(new CustomEvent("response-finished", { + detail: { responseId, status, audible, transcript }, + })); + // The slot is free now — replay a queued create (e.g. a tool follow-up + // that arrived while this response was still running). + this._flushQueuedCreate(); + break; + } + + case "response.function_call_arguments.done": { + const name = typeof event.name === "string" ? event.name : ""; + const args = typeof event.arguments === "string" ? event.arguments : "{}"; + const callId = typeof event.call_id === "string" ? event.call_id : ""; + if (name) { + this.dispatchEvent(new CustomEvent("toolcall", { + detail: { name, arguments: args, callId }, + })); + } else { + // A nameless call can't be executed, so no function_call_output is + // ever sent and the model would wait forever for a result. The + // backend shouldn't emit these; warn loudly rather than stall silently. + console.warn(`[ws] function_call_arguments.done with no name (call_id=${callId}); cannot run tool — turn may stall`); + } + break; + } + + case "conversation.item.input_audio_transcription.delta": { + const delta = typeof event.delta === "string" ? event.delta : ""; + if (delta) { + // `itemId` is REUSED across a speculative continuation, so the UI + // groups both segments into one message. The delta carries the full + // cumulative transcript so far (not an increment). + this.dispatchEvent( + new CustomEvent("transcript", { + detail: { + role: "user", + text: delta, + partial: true, + itemId: typeof event.item_id === "string" ? event.item_id : "", + }, + }), + ); + } + break; + } + + case "conversation.item.input_audio_transcription.completed": { + const transcript = typeof event.transcript === "string" ? event.transcript : ""; + if (transcript) { + this.dispatchEvent( + new CustomEvent("transcript", { + detail: { + role: "user", + text: transcript, + partial: false, + itemId: typeof event.item_id === "string" ? event.item_id : "", + }, + }), + ); + } + break; + } + + case "response.audio_transcript.delta": + case "response.output_audio_transcript.delta": { + // Stream the assistant transcript live: accumulate the incremental + // deltas and push the running text to the UI. Every transcribe event we + // receive reaches the conversation, so an interrupted reply already has + // its partial text even if the `.done` never fires. + this._markAudible(); + const rid = typeof event.response_id === "string" ? event.response_id : ""; + const delta = typeof event.delta === "string" ? event.delta : ""; + if (delta) { + this._asstTranscriptByResp.set(rid, (this._asstTranscriptByResp.get(rid) || "") + delta); + // Show completed segments + the segment streaming in right now. + this.dispatchEvent( + new CustomEvent("transcript", { + detail: { role: "assistant", text: this._asstDisplay(rid), partial: true, responseId: rid }, + }), + ); + } + break; + } + + case "response.audio_transcript.done": + case "response.output_audio_transcript.done": { + const rid = typeof event.response_id === "string" ? event.response_id : ""; + // This is ONE completed segment. A response can emit several; concatenate + // them, space-separated, until response.done clears the accumulator. + const segment = + (typeof event.transcript === "string" && event.transcript) || + this._asstTranscriptByResp.get(rid) || + ""; + this._asstTranscriptByResp.delete(rid); // segment finished; next one starts fresh + if (segment) { + const prev = this._asstFullByResp.get(rid) || ""; + this._asstFullByResp.set(rid, prev ? `${prev} ${segment}` : segment); + } + const full = this._asstFullByResp.get(rid) || ""; + if (full) { + this.dispatchEvent( + new CustomEvent("transcript", { + detail: { role: "assistant", text: full, partial: false, responseId: rid }, + }), + ); + } + break; + } + + case "error": { + const err = event.error; + console.error("[ws] server error:", err); + // The "another response is already active" race: our optimistic create + // collided with a still-running response. Don't surface it — clear the + // in-flight guard and re-queue, so the create replays on the next + // response.done (never retried immediately, which would just collide + // again). + if (err?.type === "conversation_already_has_active_response" || + err?.code === "conversation_already_has_active_response") { + if (this._createInFlight) { + this._createInFlight = false; + // Re-queue a BARE create: any image on the original payload was + // already sent before this (rejected) create, so don't resend it. + this._createQueue.push({}); + } + break; + } + // Every other server error is non-fatal: surface it for logging but + // NEVER tear the socket down. Only transport failures (close / failed + // open) are fatal, and those come through their own paths. + this.dispatchEvent( + new CustomEvent("server-error", { detail: { error: new Error(err?.message ?? "Server error") } }), + ); + break; + } + } + } + + /** @param {string} b64 */ + _pushAudioDelta(b64) { + if (!this._playbackNode) return; + if (!b64) return; + this._dbgDeltas += 1; + this._dbgBytes += (b64.length * 3) >> 2; // approx decoded bytes + const bytes = base64ToBytes(b64); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const samples = new Float32Array(bytes.byteLength / 2); + let peak = 0; + for (let i = 0; i < samples.length; i++) { + const s = view.getInt16(i * 2, true); + samples[i] = s < 0 ? s / 0x8000 : s / 0x7fff; + const a = samples[i] < 0 ? -samples[i] : samples[i]; + if (a > peak) peak = a; + } + this._dbgPeak = Math.max(this._dbgPeak || 0, peak); + // Clone (no transfer list): structured cloning across the main->worklet + // boundary is more robust than transferring the buffer — some browser + // versions mis-shape the received object and the worklet would drop it. + this._playbackNode.port.postMessage({ kind: "audio", samples }); + } + + /** @param {CloseEvent} ev */ + _onWsClose(ev) { + console.log("[ws] socket closed:", ev.code, ev.reason); + if (this._status === "closed" || this._status === "error") return; + if (ev.code === 1000) { + this._setStatus("closed"); + } else { + this.dispatchEvent( + new CustomEvent("error", { + detail: { error: new Error(`WebSocket closed (${ev.code}) ${ev.reason || ""}`.trim()) }, + }), + ); + this._setStatus("error"); + } + } + + _sendSessionUpdate() { + // Minimal payload: only the bits the user is allowed to configure. + // The s2s server already defaults to server_vad, whisper-1 + // transcription, 16 kHz PCM input and 24 kHz PCM output, so we don't + // need (and must not send) `audio.input.format`, `audio.input.transcription`, + // `audio.input.turn_detection` or `audio.output.format`: the pydantic + // validator on the server rejects the whole event if any unknown or + // future-shaped sub-field shows up. + /** @type {Record} */ + const session = { + type: "realtime", + instructions: this.options.instructions, + audio: { + output: { voice: this.options.voice }, + }, + }; + // Tools are declared here; the backend already accepts them in + // session.update and emits response.function_call_arguments.done when the + // model decides to call one. Only include the keys when we actually have + // tools — the server's pydantic validator is strict about shapes. + if (this._tools.length) { + session.tools = this._tools; + session.tool_choice = "auto"; + } + this._send({ type: "session.update", session }); + } + + /** Update voice/instructions on a live session without tearing down. */ + /** @param {{ voice?: string; instructions?: string }} patch */ + updateSession(patch) { + /** @type {Record} */ + const session = { type: "realtime" }; + if (patch.instructions) session.instructions = patch.instructions; + if (patch.voice) session.audio = { output: { voice: patch.voice } }; + if (Object.keys(session).length > 1) { + this._send({ type: "session.update", session }); + } + } + + /** + * Replace the declared tool set on a live session (e.g. the user flipped a + * tool switch mid-conversation). Always sends `tools` — an empty array + * clears them — so toggling the last tool off actually removes it. + * @param {ToolDef[]} tools + */ + setTools(tools) { + this._tools = tools; + this._send({ + type: "session.update", + session: { type: "realtime", tools, tool_choice: tools.length ? "auto" : "none" }, + }); + } + + /** + * Return a tool's result to the model. Pairs with the `toolcall` event's + * `callId`. Caller follows this with `requestResponse()` so the model speaks. + * @param {string} callId + * @param {string} output Plain text / JSON string the model will read. + */ + sendToolOutput(callId, output) { + if (!callId) return; // Can't target a result without the call id. + this._send({ + type: "conversation.item.create", + item: { type: "function_call_output", call_id: callId, output }, + }); + } + + /** + * Add an image to the conversation as user content, so the vision-language + * model can see it (used by the camera tool). `dataUrl` is a + * `data:image/jpeg;base64,...` string. + * @param {string} dataUrl + */ + sendUserImage(dataUrl) { + this._send({ + type: "conversation.item.create", + item: { + type: "message", + role: "user", + content: [{ type: "input_image", image_url: dataUrl }], + }, + }); + } + + /** + * Add a text message to the conversation as user content, and trigger a + * response. Use this to type messages instead of speaking. The text is + * sent as a user message and the model generates a reply. + * @param {string} text + */ + sendUserText(text) { + if (!text) return; + this._send({ + type: "conversation.item.create", + item: { + type: "message", + role: "user", + content: [{ type: "input_text", text }], + }, + }); + this.requestResponse(); + } + + /** + * Ask the model to generate a response now (after feeding tool results). + * Serialized: if a response is already in flight we queue this request and + * replay it once the active response finishes, so we never trip the + * backend's `conversation_already_has_active_response` guard. + * + * @param {{ image?: string }} [opts] Optional `image` (a data URL) sent as a + * user `input_image` immediately before this response.create — so the frame + * travels with the create (and is deferred together with it if queued), + * rather than being added to the conversation eagerly. Used by the camera + * tool so the model sees the snapshot in the response it's about to speak. + */ + requestResponse(opts = {}) { + if (this._responseActive()) { + this._createQueue.push(opts); + if (this._debug) console.debug(`[ws] response.create queued (a response is active); pending=${this._createQueue.length}`); + return; + } + this._createResponseNow(opts); + } + + /** True while a response occupies the single backend slot. */ + _responseActive() { + return this._openResponses > 0 || this._createInFlight; + } + + /** Send a response.create immediately and arm the in-flight guard. Any image + * on the payload is added as user content right before the create. + * @param {{ image?: string }} [opts] */ + _createResponseNow(opts = {}) { + if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return; + if (opts.image) this.sendUserImage(opts.image); + this._createInFlight = true; + this._send({ type: "response.create" }); + } + + /** Replay one queued response.create if the slot is now free. Called on every + * response.done, so queued creates drain one-per-completion. */ + _flushQueuedCreate() { + if (this._createQueue.length > 0 && !this._responseActive()) { + const opts = this._createQueue.shift(); + if (this._debug) console.debug(`[ws] replaying queued response.create; remaining=${this._createQueue.length}`); + this._createResponseNow(opts); + } + } + + /** @param {boolean} muted */ + setMuted(muted) { + this._muted = muted; + } + + /** + * Mute/unmute the page's own TTS playback. When the Live Avatar's WebRTC + * stream carries the same audio (digital human running), mute the page to + * avoid double audio; otherwise (no avatar) keep it playing. + * @param {boolean} muted + */ + setPlaybackMuted(muted) { + this._playbackMuted = muted; + if (this._playbackNode) { + this._playbackNode.port.postMessage({ kind: "config", inputRate: OUTPUT_SAMPLE_RATE, muted }); + } + } + + /** + * Update the mic noise gate live (the user moved the Settings cursor). + * @param {NoiseGate} gate + */ + setNoiseGate(gate) { + this._noiseGate = gate; + this._captureNode?.port.postMessage({ kind: "gate", ...gate }); + } + + /** @param {Record} event */ + _send(event) { + if (!this._ws || this._ws.readyState !== WebSocket.OPEN) return; + this._ws.send(JSON.stringify(event)); + } + + async close() { + // Abort a queue wait in progress: flag it and wake the poll sleep so + // `_pollQueue` throws "aborted" and connect() unwinds cleanly. + this._closed = true; + if (this._queueWake) { + clearTimeout(this._queueTimer); + const wake = this._queueWake; + this._queueWake = null; + wake(); + } + if (this._joinTimer) { + clearTimeout(this._joinTimer); + this._joinTimer = 0; + } + if (this._joinReject) { + const reject = this._joinReject; + this._joinResolve = null; + this._joinReject = null; + reject(_codedError("join aborted", "aborted")); + } + this._visualiser?.stop(); + this._visualiser = null; + try { + if (this._ws && this._ws.readyState <= WebSocket.OPEN) { + this._ws.close(1000, "client closed"); + } + } catch { + // ignored + } + this._ws = null; + + try { + this._captureNode?.port.close?.(); + } catch { + // ignored + } + try { + this._micSrc?.disconnect(); + } catch { + // ignored + } + try { + this._captureNode?.disconnect(); + } catch { + // ignored + } + try { + this._micAnalyser?.disconnect(); + } catch { + // ignored + } + try { + this._outAnalyser?.disconnect(); + } catch { + // ignored + } + try { + this._playbackNode?.disconnect(); + } catch { + // ignored + } + try { + await this._ctx?.close(); + } catch { + // ignored + } + this._ctx = null; + this._captureNode = null; + this._playbackNode = null; + this._micSrc = null; + this._micAnalyser = null; + this._outAnalyser = null; + this._setStatus("closed"); + } +} diff --git a/backend/vendor/s2s-cloud/docker-compose.yml b/backend/vendor/s2s-cloud/docker-compose.yml new file mode 100644 index 0000000..5d88660 --- /dev/null +++ b/backend/vendor/s2s-cloud/docker-compose.yml @@ -0,0 +1,72 @@ +--- +services: + + llama: + image: ghcr.io/ggml-org/llama.cpp:server-cuda + command: + - -hf + - ggml-org/gemma-4-E4B-it-GGUF + - -np + - "2" + - -c + - "65536" + - -fa + - "on" + - --swa-full + - --host + - 0.0.0.0 + - --port + - "8080" + ports: + - 8080:8080/tcp + volumes: + - ./cache/:/root/.cache/ + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0'] + capabilities: [gpu] + + pipeline: + depends_on: + - llama + build: + context: . + dockerfile: ${DOCKERFILE:-Dockerfile} + command: + - speech-to-speech + - --mode + - socket + - --recv_host + - 0.0.0.0 + - --send_host + - 0.0.0.0 + - --llm_backend + - responses-api + - --model_name + - ggml-org/gemma-4-E4B-it-GGUF + - --responses_api_base_url + - http://llama:8080/v1 + - --responses_api_api_key + - "" + - --init_chat_role + - system + - --init_chat_prompt + - "You are a helpful assistant" + expose: + - 12345/tcp + - 12346/tcp + ports: + - 12345:12345/tcp + - 12346:12346/tcp + volumes: + - ./cache/:/root/.cache/ + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ['0'] + capabilities: [gpu] diff --git a/backend/vendor/s2s-cloud/docs/assets/endpoint-swap-dark.gif b/backend/vendor/s2s-cloud/docs/assets/endpoint-swap-dark.gif new file mode 100644 index 0000000..43d57dd Binary files /dev/null and b/backend/vendor/s2s-cloud/docs/assets/endpoint-swap-dark.gif differ diff --git a/backend/vendor/s2s-cloud/docs/assets/endpoint-swap-light.gif b/backend/vendor/s2s-cloud/docs/assets/endpoint-swap-light.gif new file mode 100644 index 0000000..4fd2578 Binary files /dev/null and b/backend/vendor/s2s-cloud/docs/assets/endpoint-swap-light.gif differ diff --git a/backend/vendor/s2s-cloud/logo.png b/backend/vendor/s2s-cloud/logo.png new file mode 100644 index 0000000..bc79ea9 Binary files /dev/null and b/backend/vendor/s2s-cloud/logo.png differ diff --git a/backend/vendor/s2s-cloud/mypy.ini b/backend/vendor/s2s-cloud/mypy.ini new file mode 100644 index 0000000..976ba02 --- /dev/null +++ b/backend/vendor/s2s-cloud/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +ignore_missing_imports = True diff --git a/backend/vendor/s2s-cloud/pyproject.toml b/backend/vendor/s2s-cloud/pyproject.toml new file mode 100644 index 0000000..f46ae68 --- /dev/null +++ b/backend/vendor/s2s-cloud/pyproject.toml @@ -0,0 +1,143 @@ +[build-system] +requires = ["setuptools>=77.0.3", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "speech-to-speech" +version = "0.2.11" +description = "Low-latency speech-to-speech pipeline" +readme = "README.md" +requires-python = ">=3.10" +license = "Apache-2.0" +license-files = ["LICENSE"] +authors = [ + { name = "Hugging Face" }, +] +keywords = ["speech-to-speech", "voice-agents", "speech-recognition", "text-to-speech", "openai"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Multimedia :: Sound/Audio :: Speech", + "Topic :: Scientific/Engineering :: Artificial Intelligence", +] +dependencies = [ + "fastapi>=0.115.0", + "httpx>=0.28.0", + "nltk==3.9.4", + "numpy>=1.26.0,<2.4.4; platform_system == 'Darwin'", + "numpy>=1.26.0; platform_system != 'Darwin'", + "openai==2.28.0", + "pillow>=10.0.0", + "pydantic>=2.0", + "rich>=13.0", + "scipy>=1.10.0", + "sounddevice==0.5.3; platform_system == 'Darwin'", + "sounddevice>=0.5.0; platform_system != 'Darwin'", + "soundfile>=0.13.0; platform_system == 'Darwin'", + "torch==2.11.0; platform_system == 'Darwin'", + "torch>=2.4.0; platform_system != 'Darwin'", + "torchaudio==2.11.0; platform_system == 'Darwin'", + "torchaudio>=2.4.0; platform_system != 'Darwin'", + "transformers==5.6.2; platform_system == 'Darwin'", + "transformers>=4.57.0; platform_system != 'Darwin'", + "uvicorn>=0.30.0", + "websockets>=12.0", + "nano-parakeet>=0.2.0; platform_system != 'Darwin'", + "faster-qwen3-tts[ggml]>=0.3.2; platform_system != 'Darwin' and platform_system != 'Windows'", + "faster-qwen3-tts>=0.3.2; platform_system == 'Windows'", + "lingua-language-detector>=2.0.2", + "miniaudio==1.61; platform_system == 'Darwin'", + "mlx==0.31.1; platform_system == 'Darwin'", + "mlx-audio==0.4.2; platform_system == 'Darwin'", + "mlx-lm==0.31.1; platform_system == 'Darwin'", + "mlx-metal==0.31.1; platform_system == 'Darwin'", + "misaki>=0.9.4; platform_system == 'Darwin'", + "espeakng-loader>=0.2.4; platform_system == 'Darwin'", + "spacy>=3.8.4; platform_system == 'Darwin'", + "phonemizer-fork>=3.3.2; platform_system == 'Darwin'", +] + +[project.optional-dependencies] +chattts = [ + "ChatTTS>=0.1.1", +] +facebook-mms = [ + "transformers>=4.57.0", +] +faster-whisper = [ + "faster-whisper>=1.0.3", +] +kokoro = [ + "kokoro>=0.9.2; platform_system != 'Darwin'", +] +language-detection = [ + "lingua-language-detector>=2.0.2", +] +mlx-lm = [ + "mlx-lm==0.31.1; platform_system == 'Darwin'", + "mlx-vlm==0.4.1; platform_system == 'Darwin'", +] +paraformer = [ + "funasr>=1.1.6", + "modelscope>=1.17.1", + "onnxruntime<1.24; python_version < '3.11'", +] +pocket = [ + "pocket-tts>=0.1.0", +] +websocket = [ + "websockets>=12.0", +] +whisper-mlx = [ + "lightning-whisper-mlx>=0.0.10; platform_system == 'Darwin'", +] + +[project.urls] +Homepage = "https://github.com/huggingface/speech-to-speech" +Repository = "https://github.com/huggingface/speech-to-speech" +Issues = "https://github.com/huggingface/speech-to-speech/issues" + +[project.scripts] +speech-to-speech = "speech_to_speech.s2s_pipeline:main" + +[dependency-groups] +dev = [ + "ruff", + "mypy", + "pytest", + "pytest-asyncio", + "websockets>=12.0", +] + +[tool.pytest.ini_options] +asyncio_mode = "auto" + +[tool.uv] +package = true +prerelease = "allow" + +[tool.setuptools.packages.find] +where = ["src"] +include = ["speech_to_speech*"] +exclude = ["tests*"] + +[tool.setuptools.package-data] +speech_to_speech = ["TTS/*.wav"] + +[tool.ruff] +src = ["src"] +line-length = 120 + +[tool.ruff.lint] +select = ["E", "F", "I", "W"] +ignore = ["E501"] + +[tool.mypy] +python_version = "3.10" +ignore_missing_imports = true +warn_unused_configs = true +check_untyped_defs = false diff --git a/backend/vendor/s2s-cloud/scripts/benchmark_stt.py b/backend/vendor/s2s-cloud/scripts/benchmark_stt.py new file mode 100644 index 0000000..74ecbd8 --- /dev/null +++ b/backend/vendor/s2s-cloud/scripts/benchmark_stt.py @@ -0,0 +1,385 @@ +""" +STT Benchmarking Script + +This script benchmarks different Speech-to-Text (STT) handlers to compare their performance. +Measures: inference time, warmup time, memory usage, and transcription quality. + +Usage: + python benchmark_stt.py --audio_file path/to/audio.wav --iterations 10 + python benchmark_stt.py --audio_file path/to/audio.wav --handlers whisper mlx-audio-whisper +""" + +import argparse +import json +import logging +import time +from pathlib import Path +from queue import Queue +from threading import Event +from typing import Any, Dict, List, Optional + +import numpy as np +import soundfile as sf + +from speech_to_speech.pipeline.messages import VADAudio + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +class BenchmarkResult: + """Stores benchmark results for a single STT handler.""" + + def __init__(self, handler_name: str): + self.handler_name = handler_name + self.warmup_time = 0.0 + self.inference_times: list[float] = [] + self.time_to_first_token: list[float] = [] + self.transcriptions: list[str] = [] + self.errors: list[str] = [] + + def add_inference(self, time_taken: float, transcription: Any, ttft: Optional[float] = None): + self.inference_times.append(time_taken) + self.transcriptions.append(transcription) + if ttft is not None: + self.time_to_first_token.append(ttft) + + def add_error(self, error: str): + self.errors.append(error) + + def get_stats(self) -> Dict[str, Any]: + """Calculate statistics from benchmark results.""" + if not self.inference_times: + return { + "handler": self.handler_name, + "status": "failed", + "errors": self.errors, + } + + stats = { + "handler": self.handler_name, + "warmup_time": self.warmup_time, + "avg_inference_time": np.mean(self.inference_times), + "min_inference_time": np.min(self.inference_times), + "max_inference_time": np.max(self.inference_times), + "std_inference_time": np.std(self.inference_times), + "total_iterations": len(self.inference_times), + "errors": self.errors, + "sample_transcription": self.transcriptions[0] if self.transcriptions else None, + } + + # Add time to first token stats if available + if self.time_to_first_token: + stats["avg_time_to_first_token"] = np.mean(self.time_to_first_token) + stats["min_time_to_first_token"] = np.min(self.time_to_first_token) + stats["max_time_to_first_token"] = np.max(self.time_to_first_token) + stats["std_time_to_first_token"] = np.std(self.time_to_first_token) + + return stats + + +def load_audio(audio_path: str) -> np.ndarray: + """Load audio file and return as numpy array.""" + logger.info(f"Loading audio from: {audio_path}") + audio, sample_rate = sf.read(audio_path) + + # Convert to mono if stereo + if len(audio.shape) > 1: + audio = audio.mean(axis=1) + + # Resample to 16kHz if needed (most whisper models expect 16kHz) + if sample_rate != 16000: + logger.warning(f"Audio sample rate is {sample_rate}Hz, resampling to 16000Hz") + try: + import librosa + audio = librosa.resample(audio, orig_sr=sample_rate, target_sr=16000) + except ImportError: + logger.error("librosa not installed. Please install it with: pip install librosa") + logger.error("Attempting scipy resampling as fallback...") + from scipy import signal + # Calculate resampling ratio + num_samples = int(len(audio) * 16000 / sample_rate) + audio = signal.resample(audio, num_samples) + + return audio.astype(np.float32) + + +def benchmark_handler( + handler_name: str, + audio: np.ndarray, + iterations: int, + handler_kwargs: Optional[Dict[str, Any]] = None +) -> BenchmarkResult: + """Benchmark a single STT handler.""" + logger.info(f"Benchmarking {handler_name}...") + result = BenchmarkResult(handler_name) + + try: + # Create queues and events for handler + stop_event = Event() + queue_in: Queue[Any] = Queue() + queue_out: Queue[Any] = Queue() + + handler: Any = None + if handler_name == "whisper": + from speech_to_speech.STT.whisper_stt_handler import WhisperSTTHandler + setup_kwargs = handler_kwargs or { + "model_name": "distil-whisper/distil-large-v3", + "device": "cuda", + "torch_dtype": "float16", + } + handler = WhisperSTTHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_kwargs=setup_kwargs + ) + + elif handler_name == "whisper-mlx": + from speech_to_speech.STT.lightning_whisper_mlx_handler import LightningWhisperSTTHandler + setup_kwargs = handler_kwargs or { + "model_name": "large-v3", + "device": "mps", + } + handler = LightningWhisperSTTHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_kwargs=setup_kwargs + ) + + elif handler_name == "mlx-audio-whisper": + from speech_to_speech.STT.mlx_audio_whisper_handler import MLXAudioWhisperSTTHandler + setup_kwargs = handler_kwargs or { + "model_name": "mlx-community/whisper-large-v3-turbo", + } + handler = MLXAudioWhisperSTTHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_kwargs=setup_kwargs + ) + + elif handler_name == "faster-whisper": + from speech_to_speech.STT.faster_whisper_handler import FasterWhisperSTTHandler + setup_kwargs = handler_kwargs or { + "model_name": "large-v3", + "device": "auto", + "compute_type": "float16", + } + handler = FasterWhisperSTTHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_kwargs=setup_kwargs + ) + + elif handler_name == "moonshine": + from archive.STT.moonshine_handler import MoonshineSTTHandler + handler = MoonshineSTTHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + ) + + elif handler_name == "parakeet-tdt": + from speech_to_speech.STT.parakeet_tdt_handler import ParakeetTDTSTTHandler + setup_kwargs = handler_kwargs or { + "device": "mps", + "enable_live_transcription": False, + } + handler = ParakeetTDTSTTHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_kwargs=setup_kwargs + ) + + elif handler_name == "parakeet-tdt-progressive": + from speech_to_speech.STT.parakeet_tdt_handler import ParakeetTDTSTTHandler + setup_kwargs = handler_kwargs or { + "device": "mps", + "enable_live_transcription": True, + "live_transcription_update_interval": 0.25, + } + handler = ParakeetTDTSTTHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_kwargs=setup_kwargs + ) + else: + raise ValueError(f"Unknown handler: {handler_name}") + + # Warmup is done in handler setup + logger.info(f"Handler {handler_name} initialized and warmed up") + + # Additional warmup on the actual audio (excluded from timings) + for _ in handler.process(VADAudio(audio=audio)): + pass + + # Run benchmark iterations + for i in range(iterations): + logger.info(f"Iteration {i+1}/{iterations} for {handler_name}") + + start_time = time.perf_counter() + + # Process audio + transcription = None + time_to_first_token = None + first_output = True + + for output in handler.process(VADAudio(audio=audio)): + # Measure time to first token + if first_output: + time_to_first_token = time.perf_counter() - start_time + first_output = False + + if isinstance(output, tuple): + transcription = output[0] # (text, language) + else: + transcription = output + + end_time = time.perf_counter() + + time_taken = end_time - start_time + result.add_inference(time_taken, transcription, time_to_first_token) + + ttft_str = f", TTFT: {time_to_first_token:.4f}s" if time_to_first_token else "" + text_preview = str(transcription)[:50] if transcription is not None else "(none)" + logger.info(f" Time: {time_taken:.4f}s{ttft_str}, Text: {text_preview}...") + + # Cleanup + handler.cleanup() + stop_event.set() + + except Exception as e: + logger.error(f"Error benchmarking {handler_name}: {e}", exc_info=True) + result.add_error(str(e)) + + return result + + +def print_results(results: List[BenchmarkResult]): + """Print benchmark results in a formatted table.""" + print("\n" + "="*80) + print("BENCHMARK RESULTS") + print("="*80) + + for result in results: + stats = result.get_stats() + print(f"\nHandler: {stats['handler']}") + print("-" * 80) + + if stats.get("status") == "failed": + print(" Status: FAILED") + print(f" Errors: {stats['errors']}") + continue + + print(f" Warmup Time: {stats['warmup_time']:.4f}s") + print(f" Avg Inference Time: {stats['avg_inference_time']:.4f}s") + print(f" Min Inference Time: {stats['min_inference_time']:.4f}s") + print(f" Max Inference Time: {stats['max_inference_time']:.4f}s") + print(f" Std Deviation: {stats['std_inference_time']:.4f}s") + + # Print time to first token stats if available + if 'avg_time_to_first_token' in stats: + print("\n Time to First Token:") + print(f" Avg TTFT: {stats['avg_time_to_first_token']:.4f}s") + print(f" Min TTFT: {stats['min_time_to_first_token']:.4f}s") + print(f" Max TTFT: {stats['max_time_to_first_token']:.4f}s") + print(f" Std TTFT: {stats['std_time_to_first_token']:.4f}s") + + print(f"\n Total Iterations: {stats['total_iterations']}") + print(f" Sample Transcription: {stats['sample_transcription']}") + + if stats['errors']: + print(f" Errors: {stats['errors']}") + + # Comparison table + print("\n" + "="*80) + print("COMPARISON (Average Inference Time)") + print("="*80) + + successful_results = [r for r in results if r.inference_times] + if successful_results: + sorted_results = sorted(successful_results, key=lambda x: np.mean(x.inference_times)) + + fastest = sorted_results[0] + fastest_time = np.mean(fastest.inference_times) + + for result in sorted_results: + avg_time = np.mean(result.inference_times) + speedup = avg_time / fastest_time + print(f" {result.handler_name:25s}: {avg_time:.4f}s ({speedup:.2f}x slower than fastest)") + + +def save_results(results: List[BenchmarkResult], output_file: str): + """Save benchmark results to JSON file.""" + data = { + "results": [r.get_stats() for r in results], + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + } + + with open(output_file, 'w') as f: + json.dump(data, f, indent=2) + + logger.info(f"Results saved to: {output_file}") + + +def main(): + parser = argparse.ArgumentParser(description="Benchmark STT handlers") + parser.add_argument( + "--audio_file", + type=str, + required=True, + help="Path to audio file for benchmarking" + ) + parser.add_argument( + "--handlers", + nargs="+", + default=["whisper", "whisper-mlx", "mlx-audio-whisper", "faster-whisper", "parakeet-tdt", "parakeet-tdt-progressive"], + help="List of handlers to benchmark (default: all)" + ) + parser.add_argument( + "--iterations", + type=int, + default=5, + help="Number of iterations per handler (default: 5)" + ) + parser.add_argument( + "--output", + type=str, + default="stt_benchmark_results.json", + help="Output JSON file for results (default: stt_benchmark_results.json)" + ) + + args = parser.parse_args() + + # Validate audio file exists + if not Path(args.audio_file).exists(): + logger.error(f"Audio file not found: {args.audio_file}") + return + + # Load audio + audio = load_audio(args.audio_file) + logger.info(f"Audio loaded: {len(audio)} samples, {len(audio)/16000:.2f}s duration") + + # Run benchmarks + results = [] + for handler_name in args.handlers: + result = benchmark_handler(handler_name, audio, args.iterations) + results.append(result) + + # Print and save results + print_results(results) + save_results(results, args.output) + + logger.info("Benchmarking complete!") + + +if __name__ == "__main__": + main() diff --git a/backend/vendor/s2s-cloud/scripts/benchmark_tts.py b/backend/vendor/s2s-cloud/scripts/benchmark_tts.py new file mode 100644 index 0000000..dc8490f --- /dev/null +++ b/backend/vendor/s2s-cloud/scripts/benchmark_tts.py @@ -0,0 +1,395 @@ +""" +TTS Benchmarking Script + +Benchmarks Text-to-Speech (TTS) handlers to compare performance. +Measures: warmup time, inference time, time-to-first-chunk, audio duration, and RTF. + +Usage: + python benchmark_tts.py --text "Hello world" --iterations 3 + python benchmark_tts.py --handlers kokoro qwen3 pocket_tts +""" + +import argparse +import json +import logging +import time +from queue import Queue +from threading import Event +from typing import Any, Dict, List, Optional + +import numpy as np + +from speech_to_speech.pipeline.messages import TTSInput + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + +DEFAULT_SAMPLE_RATE = 16000 +VALID_QWEN3_MLX_QUANTIZATIONS = ("bf16", "4bit", "6bit", "8bit") + + +class BenchmarkResult: + """Stores benchmark results for a single TTS handler.""" + + def __init__(self, handler_name: str): + self.handler_name = handler_name + self.warmup_time = 0.0 + self.inference_times: list[float] = [] + self.time_to_first_chunk: list[float] = [] + self.audio_durations: list[float] = [] + self.errors: list[str] = [] + + def add_inference(self, time_taken: float, audio_duration: float, ttfc: Optional[float] = None): + self.inference_times.append(time_taken) + self.audio_durations.append(audio_duration) + if ttfc is not None: + self.time_to_first_chunk.append(ttfc) + + def add_error(self, error: str): + self.errors.append(error) + + def get_stats(self) -> Dict[str, Any]: + if not self.inference_times: + return { + "handler": self.handler_name, + "status": "failed", + "errors": self.errors, + } + + avg_time = float(np.mean(self.inference_times)) + avg_audio = float(np.mean(self.audio_durations)) + avg_rtf = avg_audio / avg_time if avg_time > 0 else 0.0 + + stats = { + "handler": self.handler_name, + "warmup_time": self.warmup_time, + "avg_inference_time": avg_time, + "min_inference_time": float(np.min(self.inference_times)), + "max_inference_time": float(np.max(self.inference_times)), + "std_inference_time": float(np.std(self.inference_times)), + "avg_audio_duration": avg_audio, + "min_audio_duration": float(np.min(self.audio_durations)), + "max_audio_duration": float(np.max(self.audio_durations)), + "std_audio_duration": float(np.std(self.audio_durations)), + "avg_rtf": avg_rtf, + "total_iterations": len(self.inference_times), + "errors": self.errors, + } + + if self.time_to_first_chunk: + stats["avg_time_to_first_chunk"] = float(np.mean(self.time_to_first_chunk)) + stats["min_time_to_first_chunk"] = float(np.min(self.time_to_first_chunk)) + stats["max_time_to_first_chunk"] = float(np.max(self.time_to_first_chunk)) + stats["std_time_to_first_chunk"] = float(np.std(self.time_to_first_chunk)) + + return stats + + +def benchmark_handler( + handler_name: str, + text: str, + iterations: int, + handler_kwargs: Optional[Dict[str, Any]] = None, + language_code: Optional[str] = "en", +) -> BenchmarkResult: + logger.info(f"Benchmarking {handler_name}...") + result = BenchmarkResult(handler_name) + + try: + stop_event = Event() + should_listen = Event() + queue_in: Queue[Any] = Queue() + queue_out: Queue[Any] = Queue() + + handler: Any = None + setup_kwargs = handler_kwargs or {} + + start_setup = time.perf_counter() + + if handler_name == "kokoro": + from speech_to_speech.TTS.kokoro_handler import KokoroTTSHandler + setup_kwargs = {"device": "auto", **setup_kwargs} + handler = KokoroTTSHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_args=(should_listen,), + setup_kwargs=setup_kwargs, + ) + elif handler_name == "pocket_tts": + from speech_to_speech.TTS.pocket_tts_handler import PocketTTSHandler + setup_kwargs = {"device": "cpu", **setup_kwargs} + handler = PocketTTSHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_args=(should_listen,), + setup_kwargs=setup_kwargs, + ) + elif handler_name == "qwen3": + from speech_to_speech.TTS.qwen3_tts_handler import Qwen3TTSHandler + setup_kwargs = { + "device": "cuda", + "model_name": "Qwen/Qwen3-TTS-12Hz-0.6B-Base", + "ref_audio": "TTS/ref_audio.wav", + **setup_kwargs, + } + handler = Qwen3TTSHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_args=(should_listen,), + setup_kwargs=setup_kwargs, + ) + elif handler_name == "chatTTS": + from speech_to_speech.TTS.chatTTS_handler import ChatTTSHandler + setup_kwargs = {"device": "cuda", **setup_kwargs} + handler = ChatTTSHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_args=(should_listen,), + setup_kwargs=setup_kwargs, + ) + elif handler_name == "facebookMMS": + from speech_to_speech.TTS.facebookmms_handler import FacebookMMSTTSHandler + setup_kwargs = {"device": "cuda", "language": "en", **setup_kwargs} + handler = FacebookMMSTTSHandler( + stop_event, + queue_in=queue_in, + queue_out=queue_out, + setup_args=(should_listen,), + setup_kwargs=setup_kwargs, + ) + else: + raise ValueError(f"Unknown handler: {handler_name}") + + result.warmup_time = time.perf_counter() - start_setup + logger.info(f"Handler {handler_name} initialized and warmed up in {result.warmup_time:.3f}s") + + for i in range(iterations): + logger.info(f"Iteration {i+1}/{iterations} for {handler_name}") + start_time = time.perf_counter() + time_to_first_chunk = None + first_output = True + total_samples = 0 + + tts_input = TTSInput(text=text, language_code=language_code) + for chunk in handler.process(tts_input): + if first_output: + time_to_first_chunk = time.perf_counter() - start_time + first_output = False + if chunk is None: + continue + try: + total_samples += len(chunk) + except Exception: + pass + + end_time = time.perf_counter() + time_taken = end_time - start_time + audio_duration = total_samples / DEFAULT_SAMPLE_RATE if total_samples > 0 else 0.0 + + result.add_inference(time_taken, audio_duration, time_to_first_chunk) + ttfc_str = f", TTFC: {time_to_first_chunk:.4f}s" if time_to_first_chunk else "" + logger.info( + f" Time: {time_taken:.4f}s{ttfc_str}, Audio: {audio_duration:.2f}s, RTF: {audio_duration / time_taken if time_taken > 0 else 0:.2f}" + ) + + handler.cleanup() + stop_event.set() + + except Exception as e: + logger.error(f"Error benchmarking {handler_name}: {e}", exc_info=True) + result.add_error(str(e)) + + return result + + +def normalize_qwen3_mlx_quantizations(values: List[str] | None) -> List[str]: + if not values: + return [] + + normalized = [] + seen = set() + for value in values: + quantization = str(value).strip().lower() + if quantization in ("default", "none", ""): + quantization = "bf16" + if quantization not in VALID_QWEN3_MLX_QUANTIZATIONS: + raise ValueError( + "Unsupported qwen3 MLX quantization " + f"{value!r}. Supported values: {', '.join(VALID_QWEN3_MLX_QUANTIZATIONS)}" + ) + if quantization in seen: + continue + seen.add(quantization) + normalized.append(quantization) + return normalized + + +def build_benchmark_targets(args) -> List[tuple[str, str, Dict[str, Any]]]: + targets = [] + qwen3_quantizations = normalize_qwen3_mlx_quantizations(args.qwen3_mlx_quantizations) + + for handler_name in args.handlers: + if handler_name == "qwen3" and qwen3_quantizations: + for quantization in qwen3_quantizations: + targets.append( + ( + f"qwen3[{quantization}]", + "qwen3", + {"mlx_quantization": quantization}, + ) + ) + continue + + targets.append((handler_name, handler_name, {})) + + return targets + + +def print_results(results: List[BenchmarkResult]): + print("\n" + "=" * 80) + print("TTS BENCHMARK RESULTS") + print("=" * 80) + + for result in results: + stats = result.get_stats() + print(f"\nHandler: {stats['handler']}") + print("-" * 80) + + if stats.get("status") == "failed": + print(" Status: FAILED") + print(f" Errors: {stats['errors']}") + continue + + print(f" Warmup Time: {stats['warmup_time']:.4f}s") + print(f" Avg Inference Time: {stats['avg_inference_time']:.4f}s") + print(f" Min Inference Time: {stats['min_inference_time']:.4f}s") + print(f" Max Inference Time: {stats['max_inference_time']:.4f}s") + print(f" Std Deviation: {stats['std_inference_time']:.4f}s") + + print(f" Avg Audio Duration: {stats['avg_audio_duration']:.2f}s") + print(f" Min Audio Duration: {stats['min_audio_duration']:.2f}s") + print(f" Max Audio Duration: {stats['max_audio_duration']:.2f}s") + print(f" Std Audio Duration: {stats['std_audio_duration']:.4f}s") + print(f" Avg RTF: {stats['avg_rtf']:.2f}") + + if "avg_time_to_first_chunk" in stats: + print("\n Time to First Chunk:") + print(f" Avg TTFC: {stats['avg_time_to_first_chunk']:.4f}s") + print(f" Min TTFC: {stats['min_time_to_first_chunk']:.4f}s") + print(f" Max TTFC: {stats['max_time_to_first_chunk']:.4f}s") + print(f" Std TTFC: {stats['std_time_to_first_chunk']:.4f}s") + + print(f"\n Total Iterations: {stats['total_iterations']}") + + if stats["errors"]: + print(f" Errors: {stats['errors']}") + + print("\n" + "=" * 80) + print("COMPARISON (Average Inference Time)") + print("=" * 80) + + successful_results = [r for r in results if r.inference_times] + if successful_results: + sorted_results = sorted(successful_results, key=lambda x: np.mean(x.inference_times)) + fastest = sorted_results[0] + fastest_time = np.mean(fastest.inference_times) + + for result in sorted_results: + avg_time = np.mean(result.inference_times) + speedup = avg_time / fastest_time + print(f" {result.handler_name:25s}: {avg_time:.4f}s ({speedup:.2f}x slower than fastest)") + + +def save_results(results: List[BenchmarkResult], output_file: str): + data = { + "results": [r.get_stats() for r in results], + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"), + } + + with open(output_file, "w") as f: + json.dump(data, f, indent=2) + + logger.info(f"Results saved to: {output_file}") + + +def main(): + parser = argparse.ArgumentParser(description="Benchmark TTS handlers") + parser.add_argument( + "--text", + type=str, + default="Hello from the speech to speech benchmark. This is a latency test.", + help="Text to synthesize", + ) + parser.add_argument( + "--handlers", + nargs="+", + default=["kokoro", "qwen3", "pocket_tts"], + help="List of handlers to benchmark (kokoro, qwen3, pocket_tts, chatTTS, facebookMMS)", + ) + parser.add_argument( + "--iterations", + type=int, + default=3, + help="Number of iterations per handler (default: 3)", + ) + parser.add_argument( + "--output", + type=str, + default="tts_benchmark_results.json", + help="Output JSON file for results (default: tts_benchmark_results.json)", + ) + parser.add_argument( + "--language_code", + type=str, + default="en", + help="Language code to pass to TTS handlers (default: en)", + ) + parser.add_argument( + "--qwen3_mlx_quantizations", + nargs="+", + default=None, + help=( + "Optional list of Apple Silicon MLX Qwen3-TTS quantizations to benchmark " + "as separate variants. Supported values: bf16, 4bit, 6bit, 8bit." + ), + ) + + args = parser.parse_args() + + if not args.handlers: + logger.error("No handlers provided") + return + + try: + targets = build_benchmark_targets(args) + except ValueError as e: + logger.error(str(e)) + return + + results = [] + for result_name, handler_name, handler_kwargs in targets: + result = benchmark_handler( + handler_name, + args.text, + args.iterations, + handler_kwargs=handler_kwargs, + language_code=args.language_code, + ) + result.handler_name = result_name + results.append(result) + + print_results(results) + save_results(results, args.output) + + logger.info("TTS benchmarking complete!") + + +if __name__ == "__main__": + main() diff --git a/backend/vendor/s2s-cloud/scripts/listen_and_play.py b/backend/vendor/s2s-cloud/scripts/listen_and_play.py new file mode 100644 index 0000000..68f212c --- /dev/null +++ b/backend/vendor/s2s-cloud/scripts/listen_and_play.py @@ -0,0 +1,136 @@ +import socket +import threading +from dataclasses import dataclass, field +from queue import Queue + +import numpy as np +import sounddevice as sd +from transformers import HfArgumentParser + + +@dataclass +class ListenAndPlayArguments: + send_rate: int = field(default=16000, metadata={"help": "In Hz. Default is 16000."}) + recv_rate: int = field(default=16000, metadata={"help": "In Hz. Default is 16000."}) + list_play_chunk_size: int = field( + default=1024, + metadata={"help": "The size of data chunks (in bytes). Default is 1024."}, + ) + host: str = field( + default="localhost", + metadata={ + "help": "The hostname or IP address for listening and playing. Default is 'localhost'." + }, + ) + send_port: int = field( + default=12345, + metadata={"help": "The network port for sending data. Default is 12345."}, + ) + recv_port: int = field( + default=12346, + metadata={"help": "The network port for receiving data. Default is 12346."}, + ) + + +def listen_and_play( + send_rate=16000, + recv_rate=44100, + list_play_chunk_size=1024, + host="localhost", + send_port=12345, + recv_port=12346, +): + send_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + send_socket.connect((host, send_port)) + + recv_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + recv_socket.connect((host, recv_port)) + + print("Recording and streaming...") + + stop_event = threading.Event() + recv_queue = Queue() + send_queue = Queue() + + # Pre-generate a static dither buffer (±1 LSB, -96 dB) to keep the audio + # sink active without calling numpy inside the real-time audio callback. + dither_bytes = np.random.randint( + -1, 2, size=list_play_chunk_size, dtype=np.int16 + ).tobytes() + + def callback_recv(outdata, frames, time, status): + if not recv_queue.empty(): + data = recv_queue.get() + outdata[: len(data)] = data + outdata[len(data) :] = b"\x00" * (len(outdata) - len(data)) + else: + outdata[:] = dither_bytes + + def callback_send(indata, frames, time, status): + if recv_queue.empty(): + data = bytes(indata) + send_queue.put(data) + + def send(stop_event, send_queue): + while not stop_event.is_set(): + data = send_queue.get() + send_socket.sendall(data) + + def recv(stop_event, recv_queue): + def receive_full_chunk(conn, chunk_size): + data = b"" + while len(data) < chunk_size: + packet = conn.recv(chunk_size - len(data)) + if not packet: + return None # Connection has been closed + data += packet + return data + + while not stop_event.is_set(): + data = receive_full_chunk(recv_socket, list_play_chunk_size * 2) + if data: + recv_queue.put(data) + + try: + send_stream = sd.RawInputStream( + samplerate=send_rate, + channels=1, + dtype="int16", + blocksize=list_play_chunk_size, + callback=callback_send, + ) + recv_stream = sd.RawOutputStream( + samplerate=recv_rate, + channels=1, + dtype="int16", + blocksize=list_play_chunk_size, + callback=callback_recv, + ) + threading.Thread(target=send_stream.start).start() + threading.Thread(target=recv_stream.start).start() + + send_thread = threading.Thread(target=send, args=(stop_event, send_queue)) + send_thread.start() + recv_thread = threading.Thread(target=recv, args=(stop_event, recv_queue)) + recv_thread.start() + + input("Press Enter to stop...") + + except KeyboardInterrupt: + print("Finished streaming.") + + finally: + stop_event.set() + # Given that socket::recv is blocking in receive_data_chunk, shut it down to allow the thread to continue. + recv_socket.shutdown(socket.SHUT_RDWR) + recv_thread.join() + send_thread.join() + send_socket.close() + recv_socket.close() + print("Connection closed.") + + +if __name__ == "__main__": + parser = HfArgumentParser((ListenAndPlayArguments,)) # type: ignore[arg-type] + (listen_and_play_kwargs,) = parser.parse_args_into_dataclasses() + listen_and_play(**vars(listen_and_play_kwargs)) diff --git a/backend/vendor/s2s-cloud/scripts/listen_and_play_realtime.py b/backend/vendor/s2s-cloud/scripts/listen_and_play_realtime.py new file mode 100644 index 0000000..ac04f44 --- /dev/null +++ b/backend/vendor/s2s-cloud/scripts/listen_and_play_realtime.py @@ -0,0 +1,385 @@ +import argparse +import asyncio +import base64 +import time +from dataclasses import dataclass, field +from queue import Empty, Queue +from threading import Event, Lock +from typing import Any, Optional + +from openai import AsyncOpenAI + + +@dataclass +class ListenAndPlayRealtimeArguments: + host: str = field( + default="192.168.1.9", + metadata={"help": "Realtime server host. Default is 192.168.1.9."}, + ) + port: int = field( + default=8765, + metadata={"help": "Realtime server port. Default is 8765."}, + ) + model: str = field( + default="local", + metadata={"help": "Model name sent to the OpenAI-compatible realtime client."}, + ) + api_key: str = field( + default="test-key", + metadata={"help": "API key for the OpenAI SDK client. Local server ignores it."}, + ) + base_url: Optional[str] = field( + default=None, + metadata={"help": "Optional HTTP base URL, e.g. http://192.168.1.9:8765/v1"}, + ) + websocket_base_url: Optional[str] = field( + default=None, + metadata={"help": "Optional WS base URL, e.g. ws://192.168.1.9:8765/v1"}, + ) + send_rate: int = field( + default=16000, + metadata={"help": "Microphone sample rate in Hz. Default is 16000."}, + ) + recv_rate: int = field( + default=16000, + metadata={"help": "Speaker sample rate in Hz. Default is 16000."}, + ) + chunk_size: int = field( + default=1024, + metadata={"help": "Audio callback block size in samples. Default is 1024."}, + ) + input_device: Optional[int] = field( + default=None, + metadata={"help": "Optional sounddevice input device index."}, + ) + output_device: Optional[int] = field( + default=None, + metadata={"help": "Optional sounddevice output device index."}, + ) + instructions: Optional[str] = field( + default=None, + metadata={"help": "Optional session instructions to apply on connect."}, + ) + voice: Optional[str] = field( + default=None, + metadata={ + "help": ( + "TTS voice sent as session.audio.output.voice. " + "Local Kokoro: e.g. bm_fable, af_heart, am_adam. " + "OpenAI Realtime: e.g. marin, cedar, alloy." + ), + }, + ) + print_json: bool = field( + default=False, + metadata={"help": "Print raw event payloads in addition to friendly logs."}, + ) + block_mic_during_playback: bool = field( + default=False, + metadata={ + "help": "If set, pause microphone capture while speaker audio is playing. Disabled by default so barge-in works." + }, + ) + + +def _make_client(args: ListenAndPlayRealtimeArguments) -> AsyncOpenAI: + base_url = args.base_url or f"http://{args.host}:{args.port}/v1" + websocket_base_url = args.websocket_base_url or f"ws://{args.host}:{args.port}/v1" + return AsyncOpenAI( + api_key=args.api_key, + base_url=base_url, + websocket_base_url=websocket_base_url, + ) + + +def _build_session_update(args: ListenAndPlayRealtimeArguments) -> dict: + def maybe_pcm_format(rate: int) -> Optional[dict]: + # The OpenAI realtime Pydantic models only validate audio/pcm at 24 kHz. + # Our local pipeline defaults to 16 kHz internally when format is omitted, + # so omit the field for the common local case instead of sending an + # invalid 16 kHz declaration. + if rate == 16000: + return None + if rate == 24000: + return {"type": "audio/pcm", "rate": 24000} + raise ValueError( + f"Unsupported rate {rate}. Use 16000 for the local pipeline default " + f"or 24000 to match the OpenAI realtime audio format schema." + ) + + input_cfg = { + "turn_detection": {"type": "server_vad", "interrupt_response": True}, + } + output_cfg: dict[str, Any] = {} + + input_format = maybe_pcm_format(args.send_rate) + output_format = maybe_pcm_format(args.recv_rate) + if input_format is not None: + input_cfg["format"] = input_format + if output_format is not None: + output_cfg["format"] = output_format + if args.voice: + output_cfg["voice"] = args.voice + + session = { + "type": "realtime", + "audio": { + "input": input_cfg, + "output": output_cfg, + }, + } + if args.instructions: + session["instructions"] = args.instructions + return {"type": "session.update", "session": session} + + +async def listen_and_play_realtime(args: ListenAndPlayRealtimeArguments) -> None: + import sounddevice as sd + + client = _make_client(args) + + mic_queue: Queue[bytes] = Queue(maxsize=128) + stop_event = Event() + playback_buffer = bytearray() + playback_lock = Lock() + speaker_active_until = [0.0] + partial_user_text = "" + live_user_width = 0 + saw_user_speech = False + + def render_live_user_text(text: str, final: bool = False) -> None: + nonlocal live_user_width + line = f"USER: {text}" + padded = line + if live_user_width > len(line): + padded += " " * (live_user_width - len(line)) + + if final: + print(f"\r{padded}", flush=True) + live_user_width = 0 + return + + print(f"\r{padded}", end="", flush=True) + live_user_width = len(line) + + def clear_live_user_text() -> None: + nonlocal live_user_width + if live_user_width == 0: + return + print("\r" + (" " * live_user_width) + "\r", end="", flush=True) + live_user_width = 0 + + def clear_playback_buffer() -> None: + speaker_active_until[0] = 0.0 + with playback_lock: + playback_buffer.clear() + + def callback_recv(outdata, _frames, _time_info, status): + if status: + print(f"Speaker status: {status}", flush=True) + + needed = len(outdata) + with playback_lock: + available = min(needed, len(playback_buffer)) + if available: + outdata[:available] = playback_buffer[:available] + del playback_buffer[:available] + if available < needed: + outdata[available:] = b"\x00" * (needed - available) + + def callback_send(indata, _frames, _time_info, status): + if status: + print(f"Mic status: {status}", flush=True) + + if args.block_mic_during_playback: + with playback_lock: + speaker_active = bool(playback_buffer) + if speaker_active or time.monotonic() < speaker_active_until[0]: + return + + try: + mic_queue.put_nowait(bytes(indata)) + except Exception: + pass + + async def send_audio(conn): + while not stop_event.is_set(): + try: + chunk = await asyncio.to_thread(mic_queue.get, True, 0.1) + except Empty: + continue + + await conn.send( + { + "type": "input_audio_buffer.append", + "audio": base64.b64encode(chunk).decode("ascii"), + } + ) + + async def receive_events(conn): + nonlocal partial_user_text, saw_user_speech + + while not stop_event.is_set(): + event = await conn.recv() + + if args.print_json: + try: + print(f"EVENT: {event.model_dump_json()}", flush=True) + except Exception: + print(f"EVENT: {event}", flush=True) + + if event.type == "session.created": + print("Connected.", flush=True) + elif event.type == "input_audio_buffer.speech_started": + clear_playback_buffer() + partial_user_text = "" + if saw_user_speech: + print("", flush=True) + saw_user_speech = True + elif event.type == "input_audio_buffer.speech_stopped": + pass + elif event.type == "conversation.item.input_audio_transcription.delta": + # This server currently sends the latest partial hypothesis in + # each "delta" event rather than a token-level suffix, so render + # the newest snapshot instead of concatenating repeated text. + partial_user_text = event.delta.strip() + if partial_user_text: + render_live_user_text(partial_user_text) + elif event.type == "conversation.item.input_audio_transcription.completed": + partial_user_text = "" + render_live_user_text(event.transcript.strip(), final=True) + elif event.type == "response.created": + clear_live_user_text() + print("ASSISTANT: ", flush=True) + elif event.type == "response.output_audio.delta": + audio = base64.b64decode(event.delta) + with playback_lock: + playback_buffer.extend(audio) + speaker_active_until[0] = time.monotonic() + max(0.15, len(audio) / (2 * args.recv_rate)) + elif event.type == "response.output_audio.done": + print("ASSISTANT: