diff --git a/.codex/environments/environment.toml b/.codex/environments/environment.toml new file mode 100644 index 0000000..602e7d1 --- /dev/null +++ b/.codex/environments/environment.toml @@ -0,0 +1,6 @@ +# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY +version = 1 +name = "grok2api" + +[setup] +script = "chcp 65001" diff --git a/app/api/pages/public.py b/app/api/pages/public.py index 0792df9..4a5fc4c 100644 --- a/app/api/pages/public.py +++ b/app/api/pages/public.py @@ -42,3 +42,37 @@ async def public_video(): if not is_public_enabled(): raise HTTPException(status_code=404, detail="Not Found") return FileResponse(STATIC_DIR / "public/pages/video.html") + + +@router.get("/nsfw", include_in_schema=False) +async def public_nsfw(): + if not is_public_enabled(): + raise HTTPException(status_code=404, detail="Not Found") + return FileResponse(STATIC_DIR / "public/pages/nsfw.html") + + +@router.get("/imagine-workbench", include_in_schema=False) +async def public_imagine_workbench(): + if not is_public_enabled(): + raise HTTPException(status_code=404, detail="Not Found") + return FileResponse(STATIC_DIR / "public/pages/imagine_workbench.html") + + +@router.get("/manifest.webmanifest", include_in_schema=False) +async def public_manifest(): + if not is_public_enabled(): + raise HTTPException(status_code=404, detail="Not Found") + return FileResponse( + STATIC_DIR / "public/manifest.webmanifest", + media_type="application/manifest+json", + ) + + +@router.get("/sw.js", include_in_schema=False) +async def public_service_worker(): + if not is_public_enabled(): + raise HTTPException(status_code=404, detail="Not Found") + return FileResponse( + STATIC_DIR / "public/sw.js", + media_type="application/javascript", + ) diff --git a/app/api/v1/admin_api/config.py b/app/api/v1/admin_api/config.py index f843b76..6148e4a 100644 --- a/app/api/v1/admin_api/config.py +++ b/app/api/v1/admin_api/config.py @@ -4,7 +4,12 @@ from fastapi import APIRouter, Depends, HTTPException from app.core.auth import verify_app_key from app.core.config import config -from app.core.storage import get_storage, LocalStorage, RedisStorage, SQLStorage +from app.core.storage import ( + get_storage as get_storage_backend, + LocalStorage, + RedisStorage, + SQLStorage, +) router = APIRouter() @@ -33,11 +38,11 @@ async def update_config(data: dict): @router.get("/storage", dependencies=[Depends(verify_app_key)]) -async def get_storage(): +async def admin_get_storage(): """获取当前存储模式""" storage_type = os.getenv("SERVER_STORAGE_TYPE", "").lower() if not storage_type: - storage = get_storage() + storage = get_storage_backend() if isinstance(storage, LocalStorage): storage_type = "local" elif isinstance(storage, RedisStorage): diff --git a/app/api/v1/files.py b/app/api/v1/files.py index 0c630bb..cf2afcb 100644 --- a/app/api/v1/files.py +++ b/app/api/v1/files.py @@ -3,6 +3,7 @@ """ import aiofiles.os +from urllib.parse import unquote from pathlib import Path from fastapi import APIRouter, HTTPException from fastapi.responses import FileResponse @@ -18,13 +19,28 @@ IMAGE_DIR = BASE_DIR / "image" VIDEO_DIR = BASE_DIR / "video" +def _normalize_cached_filename(filename: str) -> str: + """ + 规范化客户端传入文件名,兼容尾部误带反斜杠等情况。 + """ + value = (filename or "").strip() + # 尝试解码一次,兼容 %5C 这类编码 + try: + value = unquote(value) + except Exception: + pass + value = value.strip().strip('"').strip("'").rstrip("\\/") + # 将路径分隔符统一扁平化到缓存命名规则 + value = value.replace("\\", "-").replace("/", "-") + return value + + @router.get("/image/{filename:path}") async def get_image(filename: str): """ 获取图片文件 """ - if "/" in filename: - filename = filename.replace("/", "-") + filename = _normalize_cached_filename(filename) file_path = IMAGE_DIR / filename @@ -52,8 +68,7 @@ async def get_video(filename: str): """ 获取视频文件 """ - if "/" in filename: - filename = filename.replace("/", "-") + filename = _normalize_cached_filename(filename) file_path = VIDEO_DIR / filename diff --git a/app/api/v1/image.py b/app/api/v1/image.py index a7dcc9c..bfe3c82 100644 --- a/app/api/v1/image.py +++ b/app/api/v1/image.py @@ -193,15 +193,21 @@ def validate_edit_request(request: ImageEditRequest, images: List[UploadFile]): code="model_not_supported", ) _validate_common_request(request, allow_ws_stream=False) + if request.n != 1: + raise ValidationException( + message="For image edits, n must be 1.", + param="n", + code="invalid_n", + ) if not images: raise ValidationException( message="Image is required", param="image", code="missing_image", ) - if len(images) > 16: + if len(images) > 3: raise ValidationException( - message="Too many images. Maximum is 16.", + message="Too many images. Maximum is 3.", param="image", code="invalid_image_count", ) @@ -302,7 +308,8 @@ async def create_image(request: ImageGenerationRequest): @router.post("/images/edits") async def edit_image( prompt: str = Form(...), - image: List[UploadFile] = File(...), + image: Optional[List[UploadFile]] = File(None), + image_bracket: Optional[List[UploadFile]] = File(None, alias="image[]"), model: Optional[str] = Form("grok-imagine-1.0-edit"), n: int = Form(1), size: str = Form("1024x1024"), @@ -347,6 +354,13 @@ async def edit_image( if edit_request.stream is None: edit_request.stream = False + # 兼容两种多文件字段:image / image[] + upload_images: List[UploadFile] = [] + if image: + upload_images.extend(image) + if image_bracket: + upload_images.extend(image_bracket) + response_format = resolve_response_format(edit_request.response_format) if response_format == "base64": response_format = "b64_json" @@ -354,13 +368,13 @@ async def edit_image( response_field = response_field_name(response_format) # 参数验证 - validate_edit_request(edit_request, image) + validate_edit_request(edit_request, upload_images) max_image_bytes = 50 * 1024 * 1024 allowed_types = {"image/png", "image/jpeg", "image/webp", "image/jpg"} images: List[str] = [] - for item in image: + for item in upload_images: content = await item.read() await item.close() if not content: diff --git a/app/api/v1/nsfw.py b/app/api/v1/nsfw.py new file mode 100644 index 0000000..9085c1a --- /dev/null +++ b/app/api/v1/nsfw.py @@ -0,0 +1,606 @@ +""" +NSFW 全流程 API 路由 +""" + +import re +import asyncio +import math +from typing import Any, Dict, List, Optional + +from fastapi import APIRouter, HTTPException, Request +from pydantic import BaseModel, Field + +from app.core.config import get_config +from app.core.exceptions import AppException, ErrorType, ValidationException +from app.core.logger import logger +from app.services.grok.services.image import ImageGenerationService +from app.services.grok.services.model import ModelService +from app.services.grok.services.video import VideoService, VideoCollectProcessor +from app.services.token import get_token_manager + + +router = APIRouter(tags=["NSFW"]) + +RATIO_TO_SIZE = { + "16:9": "1280x720", + "9:16": "720x1280", + "3:2": "1792x1024", + "2:3": "1024x1792", + "1:1": "1024x1024", +} + +class NSFWRequest(BaseModel): + """NSFW 全流程请求""" + + image_prompt: str = Field(..., description="图片提示词") + video_prompt: Optional[str] = Field(None, description="视频提示词(可选)") + aspect_ratio: Optional[str] = Field( + None, description="比例: 16:9/9:16/3:2/2:3/1:1" + ) + ratio: Optional[str] = Field( + None, description="比例别名: 16:9/9:16/3:2/2:3/1:1" + ) + size: Optional[str] = Field( + None, + description="尺寸别名: 1280x720/720x1280/1792x1024/1024x1792/1024x1024", + ) + image_parallel: int = Field(4, ge=1, le=8, description="并发生图数") + video_parallel: int = Field(4, ge=1, le=8, description="并发视频数") + max_image_attempts: int = Field(20, ge=1, le=50, description="生图最大尝试次数") + video_length: int = Field(6, ge=5, le=15, description="视频秒数") + resolution: str = Field("480p", description="视频分辨率: 480p/720p") + preset: str = Field("spicy", description="视频风格: fun/normal/spicy") + upscale: bool = Field(True, description="视频完成后是否尝试超分") + image_only: bool = Field(False, description="仅生图不生视频") + parent_post_only: bool = Field( + True, description="仅使用 parentPostId 路径,不回退上传图片路径" + ) + text_video_fallback: bool = Field( + True, description="parentPostId 空结果时,回退普通文字生视频" + ) + parent_post_empty_retry: int = Field( + 3, ge=1, le=10, description="parentPostId 空结果重试次数" + ) + parent_post_empty_retry_delay: float = Field( + 2.0, ge=0.0, le=30.0, description="parentPostId 空结果重试间隔(秒)" + ) + + +def _normalize_ratio(raw: str) -> str: + ratio = (raw or "").strip() + ratio = ratio.replace(":", ":").replace("/", ":").replace(" ", "") + # 兼容把 16x9 误传成比例写法 + m = re.match(r"^(\d+)[xX](\d+)$", ratio) + if m: + ratio = f"{m.group(1)}:{m.group(2)}" + if ratio not in RATIO_TO_SIZE: + raise ValidationException( + message="aspect_ratio must be one of 16:9/9:16/3:2/2:3/1:1", + param="aspect_ratio", + code="invalid_aspect_ratio", + ) + return ratio + + +def _resolve_ratio(data: NSFWRequest) -> str: + """兼容 aspect_ratio / ratio / size 三种输入方式。""" + raw_ratio = (data.aspect_ratio or "").strip() or (data.ratio or "").strip() + raw_ratio = raw_ratio.replace(":", ":").replace("/", ":") + if not raw_ratio: + # 兼容把 size 直接传成比例字符串(如 16:9) + maybe_ratio_in_size = (data.size or "").strip() + maybe_ratio_in_size = maybe_ratio_in_size.replace(":", ":").replace("/", ":") + if ":" in maybe_ratio_in_size: + raw_ratio = maybe_ratio_in_size + if raw_ratio: + return _normalize_ratio(raw_ratio) + + raw_size = (data.size or "").strip() + raw_size = raw_size.replace(":", ":").replace("*", "x") + if raw_size: + # 先精确映射常见尺寸 + for ratio, size in RATIO_TO_SIZE.items(): + if size == raw_size: + return ratio + + # 再兼容任意 WxH(如 1536x864) + m = re.match(r"^\s*(\d+)\s*[xX]\s*(\d+)\s*$", raw_size) + if m: + w = int(m.group(1)) + h = int(m.group(2)) + if w > 0 and h > 0: + v = w / h + candidates = { + "16:9": 16 / 9, + "9:16": 9 / 16, + "3:2": 3 / 2, + "2:3": 2 / 3, + "1:1": 1.0, + } + # 选最近的合法比例 + return min(candidates.keys(), key=lambda k: math.fabs(candidates[k] - v)) + + return "2:3" + + +def _normalize_image_url(url: str) -> str: + app_url = (get_config("app.app_url") or "").strip().rstrip("/") + if url.startswith("http://") or url.startswith("https://"): + return url + if not url.startswith("/"): + url = f"/{url}" + if app_url: + return f"{app_url}{url}" + return url + + +def _extract_video_urls(content: str) -> tuple[str, str]: + if not content: + return "", "" + src_match = re.search(r']+src="([^"]+)"', content, re.IGNORECASE) + poster_match = re.search(r'poster="([^"]+)"', content, re.IGNORECASE) + video_url = src_match.group(1) if src_match else "" + poster_url = poster_match.group(1) if poster_match else "" + if (not video_url) and content.startswith(("http://", "https://")): + video_url = content.strip() + return video_url, poster_url + + +def _clean_url(url: str) -> str: + """清洗 URL 末尾脏字符,避免客户端误请求。""" + value = (url or "").strip().strip('"').strip("'") + value = value.rstrip("\\") + return value + + +def _extract_parent_post_id(image_url: str) -> str: + """从本地图片 URL 中提取 parentPostId 候选值。""" + if not image_url: + return "" + # 形如: /v1/files/image/-final.jpg + m = re.search( + r"/v1/files/image/([0-9a-fA-F-]{32,36})(?:-[a-z]+)?\.(?:jpg|jpeg|png|webp)$", + image_url, + ) + if m: + return m.group(1) + # 兜底: 任意位置的 uuid + m = re.search(r"([0-9a-fA-F-]{32,36})", image_url) + if m: + return m.group(1) + return "" + + +def _build_imagine_public_url(parent_post_id: str) -> str: + """根据 image_id / parentPostId 生成 imagine-public 图片 URL。""" + if not parent_post_id: + return "" + return f"https://imagine-public.x.ai/imagine-public/images/{parent_post_id}.jpg" + + +def _token_tag(token: str) -> str: + raw = token[4:] if token.startswith("sso=") else token + if not raw: + return "empty" + if len(raw) <= 14: + return raw + return f"{raw[:6]}...{raw[-6:]}" + + +async def _pick_image_token() -> tuple[Any, str]: + token_mgr = await get_token_manager() + await token_mgr.reload_if_stale() + token = None + for pool_name in ModelService.pool_candidates_for_model("grok-imagine-1.0"): + token = token_mgr.get_token(pool_name) + if token: + break + if not token: + raise AppException( + message="No available tokens. Please try again later.", + error_type=ErrorType.RATE_LIMIT.value, + code="rate_limit_exceeded", + status_code=429, + ) + return token_mgr, token + + +async def _generate_nsfw_inner(data: NSFWRequest) -> Dict[str, Any]: + """ + NSFW 全流程: + 1) 并发生图(最多 max_image_attempts 轮) + 2) 选第一张图生视频(可关闭) + 3) 返回本地 URL 与汇总文本 + """ + if not data.image_prompt or not data.image_prompt.strip(): + raise ValidationException( + message="image_prompt cannot be empty", + param="image_prompt", + code="empty_image_prompt", + ) + + ratio = _resolve_ratio(data) + image_prompt = data.image_prompt.strip() + raw_video_prompt = (data.video_prompt or "").strip() + video_prompt_is_custom = VideoService.is_meaningful_video_prompt(raw_video_prompt) + video_prompt = raw_video_prompt if video_prompt_is_custom else "" + fields_set = set(getattr(data, "model_fields_set", set())) + effective_resolution = data.resolution + effective_video_length = data.video_length + + logger.info( + f"NSFW start: ratio={ratio}, image_parallel={data.image_parallel}, " + f"max_image_attempts={data.max_image_attempts}, image_only={data.image_only}, " + f"parent_post_only={data.parent_post_only}, " + f"text_video_fallback={data.text_video_fallback}, " + f"parent_post_empty_retry={data.parent_post_empty_retry}, " + f"parent_post_empty_retry_delay={data.parent_post_empty_retry_delay}" + ) + logger.info( + f"NSFW ratio input: aspect_ratio={data.aspect_ratio}, ratio={data.ratio}, size={data.size}" + ) + logger.info(f"NSFW image prompt: {image_prompt[:120]}") + logger.info(f"NSFW video prompt: {video_prompt[:120]}") + logger.info( + "NSFW video config: " + f"resolution={effective_resolution}, video_length={effective_video_length}, " + f"upscale={data.upscale}, fields_set={sorted(fields_set)}" + ) + if raw_video_prompt and not video_prompt_is_custom: + logger.info( + "NSFW video prompt treated as generic, force spicy mode with image-url-only message" + ) + if not video_prompt_is_custom: + logger.info( + "NSFW video prompt absent or generic, force spicy mode with image-url-only message" + ) + + token_mgr, token = await _pick_image_token() + model_info = ModelService.get("grok-imagine-1.0") + if not model_info: + raise AppException( + message="Model grok-imagine-1.0 not found", + error_type=ErrorType.SERVER.value, + code="model_not_found", + status_code=500, + ) + + image_service = ImageGenerationService() + images: List[str] = [] + seen = set() + + async def _generate_one() -> Optional[str]: + result = await image_service.generate( + token_mgr=token_mgr, + token=token, + model_info=model_info, + prompt=image_prompt, + n=1, + response_format="url", + size=RATIO_TO_SIZE[ratio], + aspect_ratio=ratio, + stream=False, + enable_nsfw=True, + ) + if isinstance(result.data, list) and result.data: + first = result.data[0] + if first and first != "error": + return _normalize_image_url(first) + return None + + for attempt in range(1, data.max_image_attempts + 1): + remain = max(1, data.image_parallel - len(images)) + tasks = [_generate_one() for _ in range(remain)] + results = await asyncio.gather(*tasks, return_exceptions=True) + new_count = 0 + for item in results: + if isinstance(item, Exception): + logger.warning(f"NSFW image attempt failed: {item}") + continue + if item and item not in seen: + seen.add(item) + images.append(item) + new_count += 1 + logger.info( + f"NSFW image attempt {attempt}/{data.max_image_attempts}: +{new_count}, total={len(images)}" + ) + if len(images) >= data.image_parallel: + break + + if not images: + raise AppException( + message="NSFW image generation failed after retries", + error_type=ErrorType.SERVER.value, + code="nsfw_image_failed", + status_code=502, + ) + + response_payload: Dict[str, Any] = { + "image_prompt": image_prompt, + "video_prompt": video_prompt, + "aspect_ratio": ratio, + "images": images, + "videos": [], + } + + if data.image_only: + lines = [ + f"图片提示词: {image_prompt}", + f"比例: {ratio}", + "返回链接:", + ] + for idx, u in enumerate(images, 1): + lines.append(f"{idx}. {u}") + text = "\n".join(lines) + response_payload["result"] = text + return response_payload + + sem = asyncio.Semaphore(max(1, data.video_parallel)) + + # 为并发视频任务预分配不同 token,避免并行任务争抢同一 token。 + video_pool_candidates = ModelService.pool_candidates_for_model("grok-imagine-1.0-video") + used_video_tokens: set[str] = set() + video_task_tokens: list[str] = [] + for idx in range(len(images)): + token_info = token_mgr.get_token_for_video( + resolution=effective_resolution, + video_length=effective_video_length, + pool_candidates=video_pool_candidates, + exclude=used_video_tokens, + ) + if not token_info: + raise AppException( + message=( + "No enough distinct tokens for parallel video generation. " + "Please reduce video_parallel or add more tokens." + ), + error_type=ErrorType.RATE_LIMIT.value, + code="insufficient_distinct_video_tokens", + status_code=429, + ) + task_token = token_info.token[4:] if token_info.token.startswith("sso=") else token_info.token + used_video_tokens.add(task_token) + video_task_tokens.append(task_token) + pool_name = token_mgr.get_pool_name_for_token(task_token) or "unknown" + logger.info( + f"NSFW video token assigned: task={idx + 1}, pool={pool_name}, token={_token_tag(task_token)}" + ) + + async def _gen_video(image_url: str, index: int, task_token: str) -> Dict[str, Any]: + async with sem: + task_tag = f"task={index}, token={_token_tag(task_token)}" + logger.info(f"NSFW video start: {task_tag}, image={image_url[:120]}") + parent_post_id = _extract_parent_post_id(image_url) + # 对齐你的规则: + # - 有视频提示词: mode=custom + # - 无视频提示词: mode=extremely-spicy-or-crazy + # 这里保留 task_preset 仅用于兼容旧调用链,不再作为最终 mode 决策依据。 + task_mode = "custom" if video_prompt_is_custom else "extremely-spicy-or-crazy" + task_preset = "custom" if video_prompt_is_custom else "spicy" + task_prompt = video_prompt if video_prompt_is_custom else "" + logger.info( + "NSFW video mode resolved: " + f"{task_tag}, mode={task_mode}, " + f"video_prompt_custom={video_prompt_is_custom}, " + f"video_prompt_raw_provided={bool(raw_video_prompt)}" + ) + try: + raw_video_response: Dict[str, Any] = {} + video_content = "" + video_url = "" + poster_url = "" + + # 优先直接用 parentPostId 生视频,避免额外上传图片。 + used_parent_post = False + if parent_post_id: + source_image_url = _build_imagine_public_url(parent_post_id) + logger.info( + "NSFW video parentPostId path: " + f"{task_tag}, parent_post_id={parent_post_id}, source_image_url={source_image_url}" + ) + used_parent_post = True + for p_attempt in range(1, data.parent_post_empty_retry + 1): + response_stream = await VideoService().generate_from_parent_post( + token=task_token, + prompt=task_prompt, + parent_post_id=parent_post_id, + source_image_url=source_image_url, + aspect_ratio=ratio, + video_length=effective_video_length, + resolution=effective_resolution, + preset=task_preset, + ) + raw_video_response = await VideoCollectProcessor( + "grok-imagine-1.0-video", + task_token, + upscale_on_finish=data.upscale, + ).process(response_stream) + video_content = ( + raw_video_response.get("choices", [{}])[0] + .get("message", {}) + .get("content", "") + ) + video_url, poster_url = _extract_video_urls(video_content) + if video_url: + break + logger.warning( + "NSFW video parentPostId empty result, retrying parentPost mode: " + f"{task_tag}, attempt={p_attempt}/{data.parent_post_empty_retry}" + ) + if p_attempt < data.parent_post_empty_retry: + await asyncio.sleep(data.parent_post_empty_retry_delay) + else: + logger.warning( + f"NSFW video parentPostId unavailable, fallback upload path: {task_tag}" + ) + response_stream = await VideoService().generate_from_image( + token=task_token, + prompt=task_prompt, + image_url=image_url, + aspect_ratio=ratio, + video_length=effective_video_length, + resolution=effective_resolution, + preset=task_preset, + ) + raw_video_response = await VideoCollectProcessor( + "grok-imagine-1.0-video", + task_token, + upscale_on_finish=data.upscale, + ).process(response_stream) + video_content = ( + raw_video_response.get("choices", [{}])[0] + .get("message", {}) + .get("content", "") + ) + video_url, poster_url = _extract_video_urls(video_content) + + # parentPostId 路径无产出时,自动降级到上传图片路径重试一次 + if used_parent_post and not video_url: + if data.text_video_fallback: + logger.warning( + "NSFW video parentPostId empty result, fallback text2video path: " + f"{task_tag}" + ) + text_video_prompt = ( + f"{_build_imagine_public_url(parent_post_id)} {task_prompt}".strip() + ) + response_stream = await VideoService().generate( + token=task_token, + prompt=text_video_prompt, + aspect_ratio=ratio, + video_length=effective_video_length, + resolution_name=effective_resolution, + preset=task_preset, + ) + raw_video_response = await VideoCollectProcessor( + "grok-imagine-1.0-video", + task_token, + upscale_on_finish=data.upscale, + ).process(response_stream) + video_content = ( + raw_video_response.get("choices", [{}])[0] + .get("message", {}) + .get("content", "") + ) + video_url, poster_url = _extract_video_urls(video_content) + + if data.parent_post_only: + if not video_url: + logger.warning( + "NSFW video parentPostId empty result, keep parentPost-only mode: " + f"{task_tag}" + ) + else: + if not video_url: + logger.warning( + f"NSFW video parentPostId empty result, fallback upload path: {task_tag}" + ) + response_stream = await VideoService().generate_from_image( + token=task_token, + prompt=task_prompt, + image_url=image_url, + aspect_ratio=ratio, + video_length=effective_video_length, + resolution=effective_resolution, + preset=task_preset, + ) + raw_video_response = await VideoCollectProcessor( + "grok-imagine-1.0-video", + task_token, + upscale_on_finish=data.upscale, + ).process(response_stream) + video_content = ( + raw_video_response.get("choices", [{}])[0] + .get("message", {}) + .get("content", "") + ) + video_url, poster_url = _extract_video_urls(video_content) + if used_parent_post and data.parent_post_only and not video_url: + return { + "index": index, + "image_url": image_url, + "parent_post_id": parent_post_id, + "error": "parent_post_video_not_ready", + "url": "", + "poster_url": "", + "content": video_content, + "raw_response": raw_video_response, + } + return { + "index": index, + "image_url": image_url, + "content": video_content, + "url": _clean_url(video_url), + "poster_url": _clean_url(poster_url), + "raw_response": raw_video_response, + "parent_post_id": parent_post_id, + } + except Exception as e: + logger.warning(f"NSFW video failed: {task_tag}, error={e}") + return { + "index": index, + "image_url": image_url, + "parent_post_id": parent_post_id, + "error": str(e), + "url": "", + "poster_url": "", + "content": "", + } + + video_tasks = [ + _gen_video(image_url, idx + 1, video_task_tokens[idx]) + for idx, image_url in enumerate(images) + ] + video_results = await asyncio.gather(*video_tasks, return_exceptions=False) + response_payload["videos"] = video_results + + # 只输出干净链接,避免原始 JSON 被客户端二次提取造成重复脏链接。 + clean_links: List[str] = [] + seen_links: set[str] = set() + + for u in images: + cu = _clean_url(u) + if cu and cu not in seen_links: + seen_links.add(cu) + clean_links.append(cu) + + for item in video_results: + for k in ("url", "poster_url"): + cu = _clean_url(item.get(k, "")) + if cu and cu not in seen_links: + seen_links.add(cu) + clean_links.append(cu) + + lines = ["返回链接:"] + for idx, u in enumerate(clean_links, 1): + lines.append(f"{idx}. {u}") + + response_payload["result"] = "\n".join(lines) + return response_payload + + +@router.post("/nsfw") +async def generate_nsfw(data: NSFWRequest, request: Request) -> Dict[str, Any]: + task = asyncio.create_task(_generate_nsfw_inner(data)) + try: + while True: + done, _ = await asyncio.wait({task}, timeout=0.2) + if done: + break + if await request.is_disconnected(): + logger.info( + "NSFW interrupted by client disconnect: " + f"aspect_ratio={data.aspect_ratio}, ratio={data.ratio}, size={data.size}, " + f"image_parallel={data.image_parallel}, " + f"video_parallel={data.video_parallel}" + ) + task.cancel() + raise HTTPException(status_code=499, detail="client_closed") + return await task + except asyncio.CancelledError: + logger.info("NSFW task cancelled after disconnect") + raise HTTPException(status_code=499, detail="client_closed") + finally: + if not task.done(): + logger.info("NSFW force-cancel unfinished task") + task.cancel() diff --git a/app/api/v1/public_api/imagine.py b/app/api/v1/public_api/imagine.py index 83f59c3..5121989 100644 --- a/app/api/v1/public_api/imagine.py +++ b/app/api/v1/public_api/imagine.py @@ -1,7 +1,10 @@ import asyncio +import contextlib +import re import time import uuid from typing import Optional, List, Dict, Any +from urllib.parse import urlparse import orjson from fastapi import APIRouter, Depends, HTTPException, Query, Request, WebSocket, WebSocketDisconnect @@ -13,6 +16,7 @@ from app.core.config import get_config from app.core.logger import logger from app.api.v1.image import resolve_aspect_ratio from app.services.grok.services.image import ImageGenerationService +from app.services.grok.services.image_edit import ImageEditService from app.services.grok.services.model import ModelService from app.services.token.manager import get_token_manager @@ -21,6 +25,136 @@ router = APIRouter() IMAGINE_SESSION_TTL = 600 _IMAGINE_SESSIONS: dict[str, dict] = {} _IMAGINE_SESSIONS_LOCK = asyncio.Lock() +_RATIO_ALLOWED = {"16:9", "9:16", "3:2", "2:3", "1:1"} +IMAGINE_IMAGE_TOKEN_TTL = 7200 +_IMAGINE_IMAGE_TOKENS: dict[str, dict] = {} +_IMAGINE_IMAGE_TOKENS_LOCK = asyncio.Lock() + + +def _validate_parent_post_id(value: str) -> str: + raw = (value or "").strip() + if not raw: + raise HTTPException(status_code=400, detail="parent_post_id cannot be empty") + if not re.fullmatch(r"[0-9a-fA-F-]{32,36}", raw): + raise HTTPException(status_code=400, detail="parent_post_id format is invalid") + return raw + + +def _build_imagine_public_url(parent_post_id: str) -> str: + return f"https://imagine-public.x.ai/imagine-public/images/{parent_post_id}.jpg" + + +def _extract_parent_post_id_from_url(url: str) -> str: + text = str(url or "").strip() + if not text: + return "" + if re.fullmatch(r"[0-9a-fA-F-]{32,36}", text): + return text + + # 优先匹配真正的图片产物 ID,避免误取 /users/。 + for pattern in ( + r"/generated/([0-9a-fA-F-]{32,36})(?:/|$)", + r"/imagine-public/images/([0-9a-fA-F-]{32,36})(?:\.jpg|/|$)", + r"/images/([0-9a-fA-F-]{32,36})(?:\.jpg|/|$)", + ): + match = re.search(pattern, text) + if match: + return match.group(1) + + matches = re.findall(r"([0-9a-fA-F-]{32,36})", text) + return matches[-1] if matches else "" + + +def _to_assets_url(path: str) -> str: + raw = str(path or "").strip() + if not raw: + return "" + if not raw.startswith("/"): + raw = f"/{raw}" + return f"https://assets.grok.com{raw}" + + +def _resolve_source_image_url( + image_url: str, + parent_post_id: str = "", + fallback_source_image_url: str = "", +) -> str: + raw = str(image_url or "").strip() or str(fallback_source_image_url or "").strip() + if raw: + if raw.startswith("http://") or raw.startswith("https://"): + parsed = urlparse(raw) + host = (parsed.netloc or "").lower() + path = parsed.path or "" + if "assets.grok.com" in host and path: + return _to_assets_url(path) + if "imagine-public.x.ai" in host: + return raw + marker = "/v1/files/image/" + if marker in path: + suffix = path.split(marker, 1)[1] + return _to_assets_url(suffix) + if path.startswith("/users/"): + return _to_assets_url(path) + return raw + if raw.startswith("/v1/files/image/"): + suffix = raw.split("/v1/files/image/", 1)[1] + return _to_assets_url(suffix) + if raw.startswith("/users/") or raw.startswith("users/"): + return _to_assets_url(raw) + if raw.startswith("/imagine-public/images/"): + return f"https://imagine-public.x.ai{raw}" + + parsed_parent = _extract_parent_post_id_from_url(parent_post_id) + if parsed_parent: + return _build_imagine_public_url(parsed_parent) + return "" + + +def _mask_token(token: str) -> str: + raw = str(token or "").replace("sso=", "") + if len(raw) <= 12: + return raw or "-" + return f"{raw[:6]}...{raw[-6:]}" + + +def _extract_parent_post_id_from_payload(payload: Dict[str, Any]) -> str: + if not isinstance(payload, dict): + return "" + candidates = [ + payload.get("parent_post_id"), + payload.get("parentPostId"), + payload.get("image_id"), + payload.get("imageId"), + payload.get("url"), + payload.get("image"), + ] + for value in candidates: + parent_post_id = _extract_parent_post_id_from_url(str(value or "")) + if parent_post_id: + return parent_post_id + return "" + + +def _normalize_image_input(image_base64: str, image_url: str) -> str: + raw_b64 = str(image_base64 or "").strip() + raw_url = str(image_url or "").strip() + if raw_b64: + if raw_b64.startswith("data:"): + return raw_b64 + compact = re.sub(r"\s+", "", raw_b64) + if not compact: + raise HTTPException(status_code=400, detail="image_base64 is empty") + if not re.fullmatch(r"[A-Za-z0-9+/=_-]+", compact): + raise HTTPException(status_code=400, detail="image_base64 format is invalid") + return f"data:image/png;base64,{compact}" + if raw_url: + if raw_url.startswith("http://") or raw_url.startswith("https://"): + return raw_url + raise HTTPException(status_code=400, detail="image_url must be http(s) URL") + raise HTTPException( + status_code=400, + detail="image_base64 or image_url is required for first edit", + ) async def _clean_sessions(now: float) -> None: @@ -33,6 +167,55 @@ async def _clean_sessions(now: float) -> None: _IMAGINE_SESSIONS.pop(key, None) +async def _clean_image_tokens(now: float) -> None: + expired = [ + key + for key, info in _IMAGINE_IMAGE_TOKENS.items() + if now - float(info.get("created_at") or 0) > IMAGINE_IMAGE_TOKEN_TTL + ] + for key in expired: + _IMAGINE_IMAGE_TOKENS.pop(key, None) + + +async def _bind_image_token(parent_post_id: str, token: str) -> None: + image_id = _extract_parent_post_id_from_url(parent_post_id) + token_text = str(token or "").strip() + if not image_id or not token_text: + return + now = time.time() + async with _IMAGINE_IMAGE_TOKENS_LOCK: + await _clean_image_tokens(now) + _IMAGINE_IMAGE_TOKENS[image_id] = { + "token": token_text, + "created_at": now, + } + + +async def _get_bound_image_token(parent_post_id: str) -> Optional[str]: + image_id = _extract_parent_post_id_from_url(parent_post_id) + if not image_id: + return None + now = time.time() + async with _IMAGINE_IMAGE_TOKENS_LOCK: + await _clean_image_tokens(now) + info = _IMAGINE_IMAGE_TOKENS.get(image_id) + if not info: + return None + token = str(info.get("token") or "").strip() + return token or None + + +def _normalize_imagine_ratio(value: Optional[str]) -> str: + """统一解析 imagine 比例参数,兼容 ratio 与 size 两种写法。""" + raw = str(value or "").strip() + if not raw: + return "2:3" + if raw in _RATIO_ALLOWED: + return raw + mapped = resolve_aspect_ratio(raw) + return mapped if mapped in _RATIO_ALLOWED else "2:3" + + def _parse_sse_chunk(chunk: str) -> Optional[Dict[str, Any]]: if not chunk: return None @@ -223,6 +406,11 @@ async def public_imagine_ws(websocket: WebSocket): continue if isinstance(payload, dict): payload.setdefault("run_id", run_id) + parent_post_id = _extract_parent_post_id_from_payload( + payload + ) + if parent_post_id: + await _bind_image_token(parent_post_id, token) await _send(payload) else: images = [img for img in result.data if img and img != "error"] @@ -292,9 +480,7 @@ async def public_imagine_ws(websocket: WebSocket): } ) continue - aspect_ratio = resolve_aspect_ratio( - str(payload.get("aspect_ratio") or "2:3").strip() or "2:3" - ) + aspect_ratio = _normalize_imagine_ratio(payload.get("aspect_ratio")) nsfw = payload.get("nsfw") if nsfw is not None: nsfw = bool(nsfw) @@ -354,14 +540,13 @@ async def public_imagine_sse( if session: prompt = str(session.get("prompt") or "").strip() - ratio = str(session.get("aspect_ratio") or "2:3").strip() or "2:3" + ratio = _normalize_imagine_ratio(session.get("aspect_ratio")) nsfw = session.get("nsfw") else: prompt = (prompt or "").strip() if not prompt: raise HTTPException(status_code=400, detail="Prompt cannot be empty") - ratio = str(aspect_ratio or "2:3").strip() or "2:3" - ratio = resolve_aspect_ratio(ratio) + ratio = _normalize_imagine_ratio(aspect_ratio) nsfw = request.query_params.get("nsfw") if nsfw is not None: nsfw = str(nsfw).lower() in ("1", "true", "yes", "on") @@ -386,6 +571,10 @@ async def public_imagine_sse( while True: if await request.is_disconnected(): + logger.info( + "Imagine SSE interrupted by client disconnect: " + f"task_id={task_id or '-'}, run_id={run_id}" + ) break if task_id: session_alive = await _get_session(task_id) @@ -428,6 +617,11 @@ async def public_imagine_sse( continue if isinstance(payload, dict): payload.setdefault("run_id", run_id) + parent_post_id = _extract_parent_post_id_from_payload( + payload + ) + if parent_post_id: + await _bind_image_token(parent_post_id, token) yield f"data: {orjson.dumps(payload).decode()}\n\n" else: images = [img for img in result.data if img and img != "error"] @@ -490,11 +684,213 @@ async def public_imagine_start(data: ImagineStartRequest): prompt = (data.prompt or "").strip() if not prompt: raise HTTPException(status_code=400, detail="Prompt cannot be empty") - ratio = resolve_aspect_ratio(str(data.aspect_ratio or "2:3").strip() or "2:3") + ratio = _normalize_imagine_ratio(data.aspect_ratio) task_id = await _new_session(prompt, ratio, data.nsfw) return {"task_id": task_id, "aspect_ratio": ratio} +class ImagineEditRequest(BaseModel): + prompt: str + parent_post_id: str + source_image_url: Optional[str] = None + stream: Optional[bool] = False + + +@router.post("/imagine/edit", dependencies=[Depends(verify_public_key)]) +async def public_imagine_edit(data: ImagineEditRequest, request: Request): + prompt = (data.prompt or "").strip() + if not prompt: + raise HTTPException(status_code=400, detail="Prompt cannot be empty") + + parent_post_id = _validate_parent_post_id(data.parent_post_id) + source_image_url = (data.source_image_url or "").strip() + if source_image_url and not ( + source_image_url.startswith("http://") or source_image_url.startswith("https://") + ): + raise HTTPException(status_code=400, detail="source_image_url must be http(s) URL") + if not source_image_url: + source_image_url = _build_imagine_public_url(parent_post_id) + + model_id = "grok-imagine-1.0-edit" + model_info = ModelService.get(model_id) + if not model_info or not model_info.is_image_edit: + raise HTTPException(status_code=503, detail="Image edit model is not available") + + token_mgr = await get_token_manager() + await token_mgr.reload_if_stale() + token = await _get_bound_image_token(parent_post_id) + if token: + pool_name = token_mgr.get_pool_name_for_token(token) or "-" + logger.info( + "Imagine edit token bound hit: " + f"parent_post_id={parent_post_id}, pool={pool_name}, token={_mask_token(token)}" + ) + else: + for pool_name in ModelService.pool_candidates_for_model(model_id): + token = token_mgr.get_token(pool_name) + if token: + break + if token: + logger.info( + "Imagine edit token bound miss, fallback pool token: " + f"parent_post_id={parent_post_id}, token={_mask_token(token)}" + ) + if not token: + raise HTTPException( + status_code=429, + detail="No available tokens. Please try again later.", + ) + + async def _run_once( + progress_cb=None, + ): + started_at = time.time() + result = await ImageEditService().edit_with_parent_post( + token_mgr=token_mgr, + token=token, + model_info=model_info, + prompt=prompt, + parent_post_id=parent_post_id, + source_image_url=source_image_url, + response_format="url", + stream=False, + progress_cb=progress_cb, + ) + images = result.data if isinstance(result.data, list) else [] + if not images: + raise HTTPException(status_code=502, detail="Image edit returned no results") + + image_url = str(images[0]) + generated_parent_post_id = _extract_parent_post_id_from_url(image_url) + current_parent_post_id = generated_parent_post_id or parent_post_id + if current_parent_post_id: + await _bind_image_token(current_parent_post_id, token) + current_source_image_url = _resolve_source_image_url( + image_url=image_url, + parent_post_id=current_parent_post_id, + fallback_source_image_url=source_image_url, + ) + elapsed_ms = int((time.time() - started_at) * 1000) + return { + "created": int(time.time()), + "data": [{"url": image_url}], + "parent_post_id": parent_post_id, + "generated_parent_post_id": generated_parent_post_id, + "current_parent_post_id": current_parent_post_id, + "current_source_image_url": current_source_image_url, + "elapsed_ms": elapsed_ms, + } + + if data.stream: + async def event_stream(): + queue: asyncio.Queue[dict] = asyncio.Queue() + disconnect_event = asyncio.Event() + client_disconnected = False + + async def progress_cb(event: str, payload: dict): + item = {"event": event} + if isinstance(payload, dict): + item.update(payload) + await queue.put({"type": "progress", "payload": item}) + + async def watch_disconnect(): + nonlocal client_disconnected + while True: + if await request.is_disconnected(): + client_disconnected = True + logger.info( + "Imagine edit stream interrupted by client disconnect: " + f"parent_post_id={parent_post_id}" + ) + disconnect_event.set() + break + await asyncio.sleep(0.2) + + async def runner(): + try: + await queue.put( + { + "type": "progress", + "payload": { + "event": "request_accepted", + "progress": 4, + "message": "已接收编辑请求", + }, + } + ) + body = await _run_once(progress_cb=progress_cb) + await queue.put( + { + "type": "progress", + "payload": { + "event": "completed", + "progress": 100, + "message": "编辑完成 100%", + }, + } + ) + await queue.put({"type": "result", "payload": body}) + except asyncio.CancelledError: + logger.info( + "Imagine edit stream runner cancelled: " + f"parent_post_id={parent_post_id}" + ) + raise + except Exception as e: + await queue.put( + { + "type": "error", + "payload": { + "message": str(e), + }, + } + ) + finally: + await queue.put({"type": "done", "payload": {}}) + + task = asyncio.create_task(runner()) + watch_task = asyncio.create_task(watch_disconnect()) + try: + while True: + if disconnect_event.is_set(): + break + try: + item = await asyncio.wait_for(queue.get(), timeout=0.2) + except asyncio.TimeoutError: + continue + item_type = item.get("type") + if item_type == "done": + break + payload = item.get("payload", {}) + yield ( + f"event: {item_type}\n" + f"data: {orjson.dumps(payload).decode()}\n\n" + ) + finally: + disconnect_event.set() + if not task.done(): + if client_disconnected: + logger.info( + "Imagine edit stream cancel unfinished runner task: " + f"parent_post_id={parent_post_id}" + ) + task.cancel() + with contextlib.suppress(Exception): + await task + if not watch_task.done(): + watch_task.cancel() + with contextlib.suppress(Exception): + await watch_task + + return StreamingResponse( + event_stream(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, + ) + + return await _run_once(progress_cb=None) + + class ImagineStopRequest(BaseModel): task_ids: List[str] @@ -503,3 +899,236 @@ class ImagineStopRequest(BaseModel): async def public_imagine_stop(data: ImagineStopRequest): removed = await _drop_sessions(data.task_ids or []) return {"status": "success", "removed": removed} + + +class ImagineWorkbenchEditRequest(BaseModel): + prompt: str + parent_post_id: Optional[str] = "" + source_image_url: Optional[str] = None + image_base64: Optional[str] = None + image_url: Optional[str] = None + stream: Optional[bool] = False + + +@router.post("/imagine/workbench/edit", dependencies=[Depends(verify_public_key)]) +async def public_imagine_workbench_edit(data: ImagineWorkbenchEditRequest, request: Request): + prompt = (data.prompt or "").strip() + if not prompt: + raise HTTPException(status_code=400, detail="Prompt cannot be empty") + + model_id = "grok-imagine-1.0-edit" + model_info = ModelService.get(model_id) + if not model_info or not model_info.is_image_edit: + raise HTTPException(status_code=503, detail="Image edit model is not available") + + token_mgr = await get_token_manager() + await token_mgr.reload_if_stale() + + parent_post_id_raw = str(data.parent_post_id or "").strip() + use_parent_mode = bool(parent_post_id_raw) + parent_post_id = _validate_parent_post_id(parent_post_id_raw) if use_parent_mode else "" + + token = None + if use_parent_mode: + token = await _get_bound_image_token(parent_post_id) + if token: + pool_name = token_mgr.get_pool_name_for_token(token) or "-" + logger.info( + "Imagine workbench token bound hit: " + f"parent_post_id={parent_post_id}, pool={pool_name}, token={_mask_token(token)}" + ) + + if not token: + for pool_name in ModelService.pool_candidates_for_model(model_id): + token = token_mgr.get_token(pool_name) + if token: + break + if not token: + raise HTTPException( + status_code=429, + detail="No available tokens. Please try again later.", + ) + + async def _run_once(progress_cb=None): + started_at = time.time() + edit_service = ImageEditService() + source_image_url = (data.source_image_url or "").strip() + + if use_parent_mode: + if source_image_url and not ( + source_image_url.startswith("http://") + or source_image_url.startswith("https://") + ): + raise HTTPException( + status_code=400, detail="source_image_url must be http(s) URL" + ) + if not source_image_url: + source_image_url = _build_imagine_public_url(parent_post_id) + result = await edit_service.edit_with_parent_post( + token_mgr=token_mgr, + token=token, + model_info=model_info, + prompt=prompt, + parent_post_id=parent_post_id, + source_image_url=source_image_url, + response_format="url", + stream=False, + progress_cb=progress_cb, + ) + mode = "parent_post" + else: + image_input = _normalize_image_input( + image_base64=str(data.image_base64 or ""), + image_url=str(data.image_url or ""), + ) + result = await edit_service.edit( + token_mgr=token_mgr, + token=token, + model_info=model_info, + prompt=prompt, + images=[image_input], + n=1, + response_format="url", + stream=False, + progress_cb=progress_cb, + ) + mode = "upload" + + images = result.data if isinstance(result.data, list) else [] + if not images: + raise HTTPException(status_code=502, detail="Image edit returned no results") + + image_url = str(images[0]) + generated_parent_post_id = _extract_parent_post_id_from_url(image_url) + current_parent_post_id = generated_parent_post_id or parent_post_id + if current_parent_post_id: + await _bind_image_token(current_parent_post_id, token) + current_source_image_url = _resolve_source_image_url( + image_url=image_url, + parent_post_id=current_parent_post_id, + fallback_source_image_url=source_image_url, + ) + elapsed_ms = int((time.time() - started_at) * 1000) + + return { + "created": int(time.time()), + "data": [{"url": image_url}], + "mode": mode, + "input_parent_post_id": parent_post_id if use_parent_mode else "", + "generated_parent_post_id": generated_parent_post_id, + "current_parent_post_id": current_parent_post_id, + "current_source_image_url": current_source_image_url, + "elapsed_ms": elapsed_ms, + } + + if data.stream: + async def event_stream(): + queue: asyncio.Queue[dict] = asyncio.Queue() + disconnect_event = asyncio.Event() + client_disconnected = False + + async def progress_cb(event: str, payload: dict): + item = {"event": event} + if isinstance(payload, dict): + item.update(payload) + await queue.put({"type": "progress", "payload": item}) + + async def watch_disconnect(): + nonlocal client_disconnected + while True: + if await request.is_disconnected(): + client_disconnected = True + logger.info( + "Imagine workbench stream interrupted by client disconnect: " + f"mode={'parent_post' if use_parent_mode else 'upload'}, " + f"parent_post_id={parent_post_id or '-'}" + ) + disconnect_event.set() + break + await asyncio.sleep(0.2) + + async def runner(): + try: + await queue.put( + { + "type": "progress", + "payload": { + "event": "request_accepted", + "progress": 4, + "message": "已接收编辑请求", + }, + } + ) + body = await _run_once(progress_cb=progress_cb) + await queue.put( + { + "type": "progress", + "payload": { + "event": "completed", + "progress": 100, + "message": "编辑完成 100%", + }, + } + ) + await queue.put({"type": "result", "payload": body}) + except asyncio.CancelledError: + logger.info( + "Imagine workbench stream runner cancelled: " + f"mode={'parent_post' if use_parent_mode else 'upload'}, " + f"parent_post_id={parent_post_id or '-'}" + ) + raise + except Exception as e: + await queue.put( + { + "type": "error", + "payload": { + "message": str(e), + }, + } + ) + finally: + await queue.put({"type": "done", "payload": {}}) + + task = asyncio.create_task(runner()) + watch_task = asyncio.create_task(watch_disconnect()) + try: + while True: + if disconnect_event.is_set(): + break + try: + item = await asyncio.wait_for(queue.get(), timeout=0.2) + except asyncio.TimeoutError: + continue + item_type = item.get("type") + if item_type == "done": + break + payload = item.get("payload", {}) + yield ( + f"event: {item_type}\n" + f"data: {orjson.dumps(payload).decode()}\n\n" + ) + finally: + disconnect_event.set() + if not task.done(): + if client_disconnected: + logger.info( + "Imagine workbench stream cancel unfinished runner task: " + f"mode={'parent_post' if use_parent_mode else 'upload'}, " + f"parent_post_id={parent_post_id or '-'}" + ) + task.cancel() + with contextlib.suppress(Exception): + await task + if not watch_task.done(): + watch_task.cancel() + with contextlib.suppress(Exception): + await watch_task + + return StreamingResponse( + event_stream(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "Connection": "keep-alive"}, + ) + + return await _run_once(progress_cb=None) diff --git a/app/api/v1/public_api/prompt.py b/app/api/v1/public_api/prompt.py index 374e3ed..a4390c5 100644 --- a/app/api/v1/public_api/prompt.py +++ b/app/api/v1/public_api/prompt.py @@ -1,12 +1,18 @@ -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional +import asyncio +import contextlib +import uuid -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Request from pydantic import BaseModel, Field from app.core.auth import verify_public_key +from app.core.logger import logger from app.services.grok.services.chat import ChatService router = APIRouter() +_PROMPT_ENHANCE_TASKS: dict[str, asyncio.Task] = {} +_PROMPT_ENHANCE_TASKS_LOCK = asyncio.Lock() SYSTEM_PROMPT = """你是一个智能视觉提示词增强器 + 敏感概念翻译器,专为Grok Imagine(FLUX)优化。 @@ -58,6 +64,21 @@ SYSTEM_PROMPT = """你是一个智能视觉提示词增强器 + 敏感概念翻 class PromptEnhanceRequest(BaseModel): prompt: str = Field(..., description="原始提示词") temperature: float = Field(0.7, ge=0, le=2) + request_id: Optional[str] = Field(None, description="enhance request id") + + +class PromptEnhanceStopRequest(BaseModel): + request_id: str = Field(..., description="enhance request id") + + +async def _register_prompt_enhance_task(request_id: str, task: asyncio.Task) -> None: + async with _PROMPT_ENHANCE_TASKS_LOCK: + _PROMPT_ENHANCE_TASKS[request_id] = task + + +async def _pop_prompt_enhance_task(request_id: str) -> Optional[asyncio.Task]: + async with _PROMPT_ENHANCE_TASKS_LOCK: + return _PROMPT_ENHANCE_TASKS.pop(request_id, None) def _extract_text(result: Dict[str, Any]) -> str: @@ -84,10 +105,15 @@ def _extract_text(result: Dict[str, Any]) -> str: @router.post("/prompt/enhance", dependencies=[Depends(verify_public_key)]) -async def public_prompt_enhance(data: PromptEnhanceRequest): +async def public_prompt_enhance(data: PromptEnhanceRequest, request: Request): raw_prompt = (data.prompt or "").strip() if not raw_prompt: raise HTTPException(status_code=400, detail="prompt is required") + request_id = ( + (data.request_id or "").strip() + or (request.headers.get("x-enhance-request-id") or "").strip() + or uuid.uuid4().hex + ) messages = [ {"role": "system", "content": SYSTEM_PROMPT}, @@ -96,17 +122,76 @@ async def public_prompt_enhance(data: PromptEnhanceRequest): "content": f"请基于下面的原始提示词进行增强,严格遵循你的工作流程与输出格式。\n\n原始提示词:\n{raw_prompt}", }, ] - result = await ChatService.completions( - model="grok-4.1-fast", - messages=messages, - stream=False, - temperature=float(data.temperature or 0.7), - top_p=0.95, + task = asyncio.create_task( + ChatService.completions( + model="grok-4.1-fast", + messages=messages, + stream=False, + temperature=float(data.temperature or 0.7), + top_p=0.95, + ) ) + await _register_prompt_enhance_task(request_id, task) + logger.info( + "Prompt enhance task registered: " + f"request_id={request_id}, prompt_len={len(raw_prompt)}" + ) + try: + while True: + done, _ = await asyncio.wait({task}, timeout=0.2) + if done: + break + if await request.is_disconnected(): + logger.info( + "Prompt enhance interrupted by client disconnect: " + f"request_id={request_id}, prompt_len={len(raw_prompt)}" + ) + task.cancel() + raise HTTPException(status_code=499, detail="client_closed") + result = await task + except asyncio.CancelledError: + logger.info( + "Prompt enhance upstream task cancelled: " + f"request_id={request_id}, prompt_len={len(raw_prompt)}" + ) + raise HTTPException(status_code=499, detail="client_closed") + finally: + task_ref = await _pop_prompt_enhance_task(request_id) + if task_ref and not task_ref.done(): + logger.info( + "Prompt enhance force-cancel unfinished upstream task: " + f"request_id={request_id}" + ) + task_ref.cancel() + with contextlib.suppress(Exception): + await task_ref enhanced = _extract_text(result if isinstance(result, dict) else {}) if not enhanced: raise HTTPException(status_code=502, detail="upstream returned empty content") return { "enhanced_prompt": enhanced, "model": "grok-4.1-fast", + "request_id": request_id, } + + +@router.post("/prompt/enhance/stop", dependencies=[Depends(verify_public_key)]) +async def public_prompt_enhance_stop(data: PromptEnhanceStopRequest): + request_id = (data.request_id or "").strip() + if not request_id: + raise HTTPException(status_code=400, detail="request_id is required") + + task = await _pop_prompt_enhance_task(request_id) + if not task: + logger.info(f"Prompt enhance stop ignored: request_id={request_id}, reason=not_found") + return {"status": "not_found", "request_id": request_id} + + if task.done(): + logger.info( + f"Prompt enhance stop ignored: request_id={request_id}, reason=already_done" + ) + return {"status": "already_done", "request_id": request_id} + + logger.info(f"Prompt enhance stop requested: request_id={request_id}, action=cancel") + task.cancel() + return {"status": "cancelling", "request_id": request_id} diff --git a/app/core/exceptions.py b/app/core/exceptions.py index 24aa281..201847a 100644 --- a/app/core/exceptions.py +++ b/app/core/exceptions.py @@ -91,12 +91,17 @@ class AuthenticationException(AppException): class UpstreamException(AppException): """上游服务错误""" - def __init__(self, message: str, details: Any = None): + def __init__( + self, + message: str, + details: Any = None, + status_code: int = 502, + ): super().__init__( message=message, error_type=ErrorType.SERVER.value, code="upstream_error", - status_code=502, + status_code=status_code, ) self.details = details diff --git a/app/services/grok/services/chat.py b/app/services/grok/services/chat.py index 139012a..711cc0b 100644 --- a/app/services/grok/services/chat.py +++ b/app/services/grok/services/chat.py @@ -503,6 +503,7 @@ class StreamProcessor(proc_base.BaseProcessor): yield "data: [DONE]\n\n" except asyncio.CancelledError: logger.debug("Stream cancelled by client", extra={"model": self.model}) + raise except StreamIdleTimeoutError as e: raise UpstreamException( message=f"Stream idle timeout after {e.idle_seconds}s", @@ -644,6 +645,7 @@ class CollectProcessor(proc_base.BaseProcessor): except asyncio.CancelledError: logger.debug("Collect cancelled by client", extra={"model": self.model}) + raise except StreamIdleTimeoutError as e: logger.warning(f"Collect idle timeout: {e}", extra={"model": self.model}) except RequestsError as e: diff --git a/app/services/grok/services/image_edit.py b/app/services/grok/services/image_edit.py index eba6f1f..7f77842 100644 --- a/app/services/grok/services/image_edit.py +++ b/app/services/grok/services/image_edit.py @@ -6,7 +6,7 @@ import asyncio import random import re from dataclasses import dataclass -from typing import AsyncGenerator, AsyncIterable, List, Union, Any +from typing import AsyncGenerator, AsyncIterable, List, Union, Any, Callable import orjson from curl_cffi.requests.errors import RequestsError @@ -43,6 +43,26 @@ class ImageEditResult: class ImageEditService: """Image edit orchestration service.""" + async def _emit_progress( + self, + progress_cb: Callable[[str, dict], Any] | None, + event: str, + progress: int, + message: str, + **extra: Any, + ) -> None: + if not progress_cb: + return + payload = {"progress": int(progress), "message": message} + if extra: + payload.update(extra) + try: + result = progress_cb(event, payload) + if asyncio.iscoroutine(result): + await result + except Exception as e: + logger.debug(f"Image edit progress callback ignored: {e}") + async def edit( self, *, @@ -54,6 +74,7 @@ class ImageEditService: n: int, response_format: str, stream: bool, + progress_cb: Callable[[str, dict], Any] | None = None, ) -> ImageEditResult: max_token_retries = int(get_config("retry.max_retry")) tried_tokens: set[str] = set() @@ -75,11 +96,43 @@ class ImageEditService: ) tried_tokens.add(current_token) + await self._emit_progress( + progress_cb, + "token_selected", + 8, + "已匹配编辑令牌", + ) try: + await self._emit_progress( + progress_cb, + "upload_start", + 16, + "正在上传输入图片", + ) image_urls = await self._upload_images(images, current_token) + await self._emit_progress( + progress_cb, + "upload_done", + 30, + f"图片上传完成,共 {len(image_urls)} 张", + count=len(image_urls), + ) + await self._emit_progress( + progress_cb, + "pre_create_start", + 36, + "正在创建媒体帖子", + ) parent_post_id = await self._get_parent_post_id( current_token, image_urls ) + await self._emit_progress( + progress_cb, + "pre_create_done", + 42, + "媒体帖子创建完成", + parent_post_id=parent_post_id or "", + ) model_config_override = { "modelMap": { @@ -105,6 +158,7 @@ class ImageEditService: stream=True, tool_overrides=tool_overrides, model_config_override=model_config_override, + image_generation_count=1, ) processor = ImageStreamProcessor( model_info.model_id, @@ -122,14 +176,27 @@ class ImageEditService: ), ) + await self._emit_progress( + progress_cb, + "chat_request_start", + 48, + "已提交编辑请求", + parent_post_id=parent_post_id or "", + ) images_out = await self._collect_images( token=current_token, prompt=prompt, model_info=model_info, - n=n, response_format=response_format, tool_overrides=tool_overrides, model_config_override=model_config_override, + progress_cb=progress_cb, + ) + await self._emit_progress( + progress_cb, + "collect_done", + 92, + f"已收到 {len(images_out)} 张结果", ) try: effort = ( @@ -149,6 +216,199 @@ class ImageEditService: last_error = e if rate_limited(e): await token_mgr.mark_rate_limited(current_token) + await self._emit_progress( + progress_cb, + "rate_limited", + 16, + "令牌限流,正在切换重试", + ) + logger.warning( + f"Token {current_token[:10]}... rate limited (429), " + f"trying next token (attempt {attempt + 1}/{max_token_retries})" + ) + continue + raise + + if last_error: + raise last_error + raise AppException( + message="No available tokens. Please try again later.", + error_type=ErrorType.RATE_LIMIT.value, + code="rate_limit_exceeded", + status_code=429, + ) + + async def edit_with_parent_post( + self, + *, + token_mgr: Any, + token: str, + model_info: Any, + prompt: str, + parent_post_id: str, + source_image_url: str, + response_format: str, + stream: bool, + progress_cb: Callable[[str, dict], Any] | None = None, + ) -> ImageEditResult: + """基于 parentPostId 进行编辑,不上传图片。""" + max_token_retries = int(get_config("retry.max_retry")) + tried_tokens: set[str] = set() + last_error: Exception | None = None + + for attempt in range(max_token_retries): + preferred = token if attempt == 0 else None + current_token = await pick_token( + token_mgr, model_info.model_id, tried_tokens, preferred=preferred + ) + if not current_token: + if last_error: + raise last_error + raise AppException( + message="No available tokens. Please try again later.", + error_type=ErrorType.RATE_LIMIT.value, + code="rate_limit_exceeded", + status_code=429, + ) + + tried_tokens.add(current_token) + await self._emit_progress( + progress_cb, + "token_selected", + 8, + "已匹配编辑令牌", + ) + try: + image_ref = (source_image_url or "").strip() + if not image_ref: + image_ref = ( + f"https://imagine-public.x.ai/imagine-public/images/{parent_post_id}.jpg" + ) + effective_parent_post_id = parent_post_id + await self._emit_progress( + progress_cb, + "pre_create_start", + 18, + "正在创建媒体帖子", + parent_post_id=parent_post_id, + ) + try: + # 与 nsfw 的 parentPostId 链路保持一致:先预创建 media post + # 这样上游在 imagine-image-edit 校验 parentPostId 时更稳定。 + image_post_id = await VideoService().create_image_post( + current_token, image_ref + ) + if image_post_id: + effective_parent_post_id = image_post_id + logger.info( + "Image edit(parentPostId) pre-create media post done: " + f"parent_post_id={parent_post_id}, " + f"image_post_id={effective_parent_post_id}, media_url={image_ref}" + ) + await self._emit_progress( + progress_cb, + "pre_create_done", + 34, + "媒体帖子创建完成", + image_post_id=effective_parent_post_id, + ) + except Exception as e: + logger.warning( + "Image edit(parentPostId) pre-create media post failed, continue anyway: " + f"parent_post_id={parent_post_id}, media_url={image_ref}, error={e}" + ) + await self._emit_progress( + progress_cb, + "pre_create_failed", + 28, + "媒体帖子创建失败,继续请求", + ) + + model_config_override = { + "modelMap": { + "imageEditModel": "imagine", + "imageEditModelConfig": { + "imageReferences": [image_ref], + "parentPostId": effective_parent_post_id, + }, + } + } + tool_overrides = {"imageGen": True} + + if stream: + response = await GrokChatService().chat( + token=current_token, + message=prompt, + model=model_info.grok_model, + mode=None, + stream=True, + tool_overrides=tool_overrides, + model_config_override=model_config_override, + image_generation_count=1, + ) + processor = ImageStreamProcessor( + model_info.model_id, + current_token, + n=1, + response_format=response_format, + ) + return ImageEditResult( + stream=True, + data=wrap_stream_with_usage( + processor.process(response), + token_mgr, + current_token, + model_info.model_id, + ), + ) + + await self._emit_progress( + progress_cb, + "chat_request_start", + 48, + "已提交编辑请求", + parent_post_id=effective_parent_post_id, + ) + images_out = await self._collect_images( + token=current_token, + prompt=prompt, + model_info=model_info, + response_format=response_format, + tool_overrides=tool_overrides, + model_config_override=model_config_override, + progress_cb=progress_cb, + ) + await self._emit_progress( + progress_cb, + "collect_done", + 92, + f"已收到 {len(images_out)} 张结果", + ) + try: + effort = ( + EffortType.HIGH + if (model_info and model_info.cost.value == "high") + else EffortType.LOW + ) + await token_mgr.consume(current_token, effort) + logger.debug( + "Image edit(parentPostId) completed, " + f"recorded usage (effort={effort.value})" + ) + except Exception as e: + logger.warning(f"Failed to record image edit(parentPostId) usage: {e}") + return ImageEditResult(stream=False, data=images_out) + + except UpstreamException as e: + last_error = e + if rate_limited(e): + await token_mgr.mark_rate_limited(current_token) + await self._emit_progress( + progress_cb, + "rate_limited", + 16, + "令牌限流,正在切换重试", + ) logger.warning( f"Token {current_token[:10]}... rate limited (429), " f"trying next token (attempt {attempt + 1}/{max_token_retries})" @@ -222,13 +482,11 @@ class ImageEditService: token: str, prompt: str, model_info: Any, - n: int, response_format: str, tool_overrides: dict, model_config_override: dict, + progress_cb: Callable[[str, dict], Any] | None = None, ) -> List[str]: - calls_needed = (n + 1) // 2 - async def _call_edit(): response = await GrokChatService().chat( token=token, @@ -238,47 +496,23 @@ class ImageEditService: stream=True, tool_overrides=tool_overrides, model_config_override=model_config_override, + image_generation_count=1, ) processor = ImageCollectProcessor( - model_info.model_id, token, response_format=response_format + model_info.model_id, + token, + response_format=response_format, + progress_cb=progress_cb, ) return await processor.process(response) - last_error: Exception | None = None - rate_limit_error: Exception | None = None - - if calls_needed == 1: - all_images = await _call_edit() - else: - tasks = [_call_edit() for _ in range(calls_needed)] - results = await asyncio.gather(*tasks, return_exceptions=True) - - all_images: List[str] = [] - for result in results: - if isinstance(result, Exception): - logger.error(f"Concurrent call failed: {result}") - last_error = result - if rate_limited(result): - rate_limit_error = result - elif isinstance(result, list): - all_images.extend(result) + all_images = await _call_edit() if not all_images: - if rate_limit_error: - raise rate_limit_error - if last_error: - raise last_error raise UpstreamException( "Image edit returned no results", details={"error": "empty_result"} ) - - if len(all_images) >= n: - return all_images[:n] - - selected_images = all_images.copy() - while len(selected_images) < n: - selected_images.append("error") - return selected_images + return [all_images[0]] class ImageStreamProcessor(BaseProcessor): @@ -436,16 +670,39 @@ class ImageStreamProcessor(BaseProcessor): class ImageCollectProcessor(BaseProcessor): """HTTP image non-stream processor.""" - def __init__(self, model: str, token: str = "", response_format: str = "b64_json"): + def __init__( + self, + model: str, + token: str = "", + response_format: str = "b64_json", + progress_cb: Callable[[str, dict], Any] | None = None, + ): if response_format == "base64": response_format = "b64_json" super().__init__(model, token) self.response_format = response_format + self.progress_cb = progress_cb + + async def _emit_progress( + self, event: str, progress: int, message: str, **extra: Any + ) -> None: + if not self.progress_cb: + return + payload = {"progress": int(progress), "message": message} + if extra: + payload.update(extra) + try: + result = self.progress_cb(event, payload) + if asyncio.iscoroutine(result): + await result + except Exception: + pass async def process(self, response: AsyncIterable[bytes]) -> List[str]: """Process and collect images.""" images = [] idle_timeout = get_config("image.stream_timeout") + chat_connected_emitted = False try: async for line in _with_idle_timeout(response, idle_timeout, self.model): @@ -458,6 +715,13 @@ class ImageCollectProcessor(BaseProcessor): continue resp = data.get("result", {}).get("response", {}) + if not chat_connected_emitted and resp: + chat_connected_emitted = True + await self._emit_progress( + "chat_connected", + 60, + "模型连接成功,正在生成图片", + ) if mr := resp.get("modelResponse"): if urls := _collect_images(mr): @@ -466,6 +730,13 @@ class ImageCollectProcessor(BaseProcessor): processed = await self.process_url(url, "image") if processed: images.append(processed) + progress = min(90, 64 + len(images) * 12) + await self._emit_progress( + "image_downloaded", + progress, + f"已下载第 {len(images)} 张图片", + count=len(images), + ) continue try: dl_service = self._get_dl() @@ -478,6 +749,13 @@ class ImageCollectProcessor(BaseProcessor): else: b64 = base64_data images.append(b64) + progress = min(90, 64 + len(images) * 12) + await self._emit_progress( + "image_downloaded", + progress, + f"已下载第 {len(images)} 张图片", + count=len(images), + ) except Exception as e: logger.warning( f"Failed to convert image to base64, falling back to URL: {e}" @@ -485,6 +763,13 @@ class ImageCollectProcessor(BaseProcessor): processed = await self.process_url(url, "image") if processed: images.append(processed) + progress = min(90, 64 + len(images) * 12) + await self._emit_progress( + "image_downloaded", + progress, + f"已下载第 {len(images)} 张图片", + count=len(images), + ) except asyncio.CancelledError: logger.debug("Image collect cancelled by client") diff --git a/app/services/grok/services/video.py b/app/services/grok/services/video.py index 70f477e..b6ced79 100644 --- a/app/services/grok/services/video.py +++ b/app/services/grok/services/video.py @@ -33,7 +33,7 @@ from app.services.grok.utils.retry import rate_limited from app.services.reverse.app_chat import AppChatReverse from app.services.reverse.media_post import MediaPostReverse from app.services.reverse.video_upscale import VideoUpscaleReverse -from app.services.token.manager import BASIC_POOL_NAME +from app.services.reverse.assets_list import AssetsListReverse _VIDEO_SEMAPHORE = None _VIDEO_SEM_VALUE = 0 @@ -48,12 +48,132 @@ def _get_video_semaphore() -> asyncio.Semaphore: return _VIDEO_SEMAPHORE +def _token_tag(token: str) -> str: + raw = token[4:] if token.startswith("sso=") else token + if not raw: + return "empty" + if len(raw) <= 14: + return raw + return f"{raw[:6]}...{raw[-6:]}" + + class VideoService: """Video generation service.""" def __init__(self): self.timeout = None + @staticmethod + def is_meaningful_video_prompt(prompt: str) -> bool: + """判断提示词是否属于“有效自定义视频提示词”。 + + 以下场景视为非自定义(返回 False): + - 空提示词 + - 仅“让它动起来/生成视频/animate this”等泛化短提示 + """ + text = (prompt or "").strip().lower() + if not text: + return False + + # 统一空白与常见收尾标点 + text = re.sub(r"\s+", " ", text).strip( + " \t\r\n.,!?;:,。!?;:'\"`~()[]{}<>《》「」【】" + ) + key = re.sub(r"\s+", "", text) + if not text: + return False + + generic_en = { + "animate", + "animate this", + "animate this image", + "make it move", + "make this move", + "generate video", + "make video", + "make a video", + "create video", + "turn this into a video", + "turn it into a video", + "video", + } + generic_zh = { + "动起来", + "让它动起来", + "让图片动起来", + "让这张图动起来", + "生成视频", + "生成一个视频", + "生成一段视频", + "做成视频", + "做个视频", + "制作视频", + "变成视频", + "变成一个视频", + "视频", + } + if text in generic_en or key in generic_zh: + return False + + # 英文泛化短句:please animate this / please generate a video + if re.fullmatch(r"(please\s+)?animate(\s+this(\s+image)?)?", text): + return False + if re.fullmatch( + r"(please\s+)?(make|create|generate)\s+(a\s+)?video", text + ): + return False + + # 中文泛化短句:请让它动起来 / 帮我生成视频 / 把这张图做成视频 + if re.fullmatch( + r"(请|请你|帮我|麻烦你)?(把)?(它|图片|这张图)?" + r"(动起来|生成视频|做成视频|制作视频)(吧|一下|下)?", + key, + ): + return False + + return True + + @staticmethod + def _build_video_message( + prompt: str, + preset: str = "normal", + source_image_url: str = "", + ) -> str: + """构造视频请求 message: + - 有提示词:统一走 custom,并发送 image_url + prompt + mode + - 无提示词:统一走 spicy(忽略 preset) + """ + prompt_text = (prompt or "").strip() + if not VideoService.is_meaningful_video_prompt(prompt_text): + prompt_text = "" + image_core = (source_image_url or "").strip() + if prompt_text: + if image_core: + return f"{image_core} {prompt_text} --mode=custom" + return f"{prompt_text} --mode=custom" + + mode_flag = "--mode=extremely-spicy-or-crazy" + if image_core: + return f"{image_core} {mode_flag}" + return mode_flag + + @staticmethod + def _build_imagine_public_url(parent_post_id: str) -> str: + return f"https://imagine-public.x.ai/imagine-public/images/{parent_post_id}.jpg" + + @staticmethod + def _is_moderated_line(line: bytes) -> bool: + text = _normalize_line(line) + if not text: + return False + try: + data = orjson.loads(text) + except Exception: + return False + resp = data.get("result", {}).get("response", {}) + video_resp = resp.get("streamingVideoGenerationResponse", {}) + return bool(video_resp.get("moderated") is True) + async def create_post( self, token: str, @@ -108,17 +228,17 @@ class VideoService: preset: str = "normal", ) -> AsyncGenerator[bytes, None]: """Generate video.""" + token_tag = _token_tag(token) + mode = ( + "custom" + if VideoService.is_meaningful_video_prompt(prompt) + else "extremely-spicy-or-crazy" + ) logger.info( - f"Video generation: prompt='{prompt[:50]}...', ratio={aspect_ratio}, length={video_length}s, preset={preset}" + f"Video generation: token={token_tag}, prompt='{prompt[:50]}...', ratio={aspect_ratio}, length={video_length}s, mode={mode}" ) post_id = await self.create_post(token, prompt) - mode_map = { - "fun": "--mode=extremely-crazy", - "normal": "--mode=normal", - "spicy": "--mode=extremely-spicy-or-crazy", - } - mode_flag = mode_map.get(preset, "--mode=custom") - message = f"{prompt} {mode_flag}" + message = self._build_video_message(prompt=prompt, preset=preset) model_config_override = { "modelMap": { "videoGenModelConfig": { @@ -126,34 +246,58 @@ class VideoService: "parentPostId": post_id, "resolutionName": resolution_name, "videoLength": video_length, + "isVideoEdit": False, } } } + moderated_max_retry = max(1, int(get_config("video.moderated_max_retry", 5))) async def _stream(): - session = AsyncSession() - try: - async with _get_video_semaphore(): - stream_response = await AppChatReverse.request( - session, - token, - message=message, - model="grok-3", - tool_overrides={"videoGen": True}, - model_config_override=model_config_override, - ) - logger.info(f"Video generation started: post_id={post_id}") - async for line in stream_response: - yield line - except Exception as e: + for attempt in range(1, moderated_max_retry + 1): + session = AsyncSession() + moderated_hit = False try: - await session.close() - except Exception: - pass - logger.error(f"Video generation error: {e}") - if isinstance(e, AppException): - raise - raise UpstreamException(f"Video generation error: {str(e)}") + async with _get_video_semaphore(): + stream_response = await AppChatReverse.request( + session, + token, + message=message, + model="grok-3", + tool_overrides={"videoGen": True}, + model_config_override=model_config_override, + ) + logger.info( + f"Video generation started: token={token_tag}, post_id={post_id}, attempt={attempt}/{moderated_max_retry}" + ) + async for line in stream_response: + if self._is_moderated_line(line): + moderated_hit = True + logger.warning( + f"Video generation moderated: token={token_tag}, retry {attempt}/{moderated_max_retry}" + ) + break + yield line + + if not moderated_hit: + return + if attempt < moderated_max_retry: + await asyncio.sleep(1.2) + continue + raise UpstreamException( + "Video blocked by moderation", + status_code=400, + details={"moderated": True, "attempts": moderated_max_retry}, + ) + except Exception as e: + logger.error(f"Video generation error: {e}") + if isinstance(e, AppException): + raise + raise UpstreamException(f"Video generation error: {str(e)}") + finally: + try: + await session.close() + except Exception: + pass return _stream() @@ -168,17 +312,21 @@ class VideoService: preset: str = "normal", ) -> AsyncGenerator[bytes, None]: """Generate video from image.""" + token_tag = _token_tag(token) + mode = ( + "custom" + if VideoService.is_meaningful_video_prompt(prompt) + else "extremely-spicy-or-crazy" + ) logger.info( - f"Image to video: prompt='{prompt[:50]}...', image={image_url[:80]}" + f"Image to video: token={token_tag}, prompt='{prompt[:50]}...', image={image_url[:80]}, mode={mode}" ) post_id = await self.create_image_post(token, image_url) - mode_map = { - "fun": "--mode=extremely-crazy", - "normal": "--mode=normal", - "spicy": "--mode=extremely-spicy-or-crazy", - } - mode_flag = mode_map.get(preset, "--mode=custom") - message = f"{prompt} {mode_flag}" + message = self._build_video_message( + prompt=prompt, + preset=preset, + source_image_url=image_url, + ) model_config_override = { "modelMap": { "videoGenModelConfig": { @@ -186,34 +334,178 @@ class VideoService: "parentPostId": post_id, "resolutionName": resolution, "videoLength": video_length, + "isVideoEdit": False, } } } + moderated_max_retry = max(1, int(get_config("video.moderated_max_retry", 5))) async def _stream(): - session = AsyncSession() - try: - async with _get_video_semaphore(): - stream_response = await AppChatReverse.request( - session, - token, - message=message, - model="grok-3", - tool_overrides={"videoGen": True}, - model_config_override=model_config_override, - ) - logger.info(f"Video generation started: post_id={post_id}") - async for line in stream_response: - yield line - except Exception as e: + for attempt in range(1, moderated_max_retry + 1): + session = AsyncSession() + moderated_hit = False try: - await session.close() - except Exception: - pass - logger.error(f"Video generation error: {e}") - if isinstance(e, AppException): - raise - raise UpstreamException(f"Video generation error: {str(e)}") + async with _get_video_semaphore(): + stream_response = await AppChatReverse.request( + session, + token, + message=message, + model="grok-3", + tool_overrides={"videoGen": True}, + model_config_override=model_config_override, + ) + logger.info( + f"Video generation started: token={token_tag}, post_id={post_id}, attempt={attempt}/{moderated_max_retry}" + ) + async for line in stream_response: + if self._is_moderated_line(line): + moderated_hit = True + logger.warning( + f"Video generation moderated: token={token_tag}, retry {attempt}/{moderated_max_retry}" + ) + break + yield line + + if not moderated_hit: + return + if attempt < moderated_max_retry: + await asyncio.sleep(1.2) + continue + raise UpstreamException( + "Video blocked by moderation", + status_code=400, + details={"moderated": True, "attempts": moderated_max_retry}, + ) + except Exception as e: + logger.error(f"Video generation error: {e}") + if isinstance(e, AppException): + raise + raise UpstreamException(f"Video generation error: {str(e)}") + finally: + try: + await session.close() + except Exception: + pass + + return _stream() + + async def generate_from_parent_post( + self, + token: str, + prompt: str, + parent_post_id: str, + source_image_url: str = "", + aspect_ratio: str = "3:2", + video_length: int = 6, + resolution: str = "480p", + preset: str = "normal", + ) -> AsyncGenerator[bytes, None]: + """Generate video by existing parent post ID (preferred path).""" + token_tag = _token_tag(token) + mode = ( + "custom" + if VideoService.is_meaningful_video_prompt(prompt) + else "extremely-spicy-or-crazy" + ) + logger.info( + f"ParentPost to video: token={token_tag}, prompt='{prompt[:50]}...', parent_post_id={parent_post_id}" + ) + raw_source_image_url = (source_image_url or "").strip() + source_image_url = self._build_imagine_public_url(parent_post_id) + if raw_source_image_url and raw_source_image_url != source_image_url: + logger.info( + "ParentPost source image normalized to imagine-public: " + f"token={token_tag}, parent_post_id={parent_post_id}, " + f"raw_source_image_url={raw_source_image_url}, normalized_source_image_url={source_image_url}" + ) + + # 对齐官网全链路:先创建 IMAGE 类型 media post,再触发 conversations/new。 + # 注意:videoGenModelConfig.parentPostId 仍使用 imagine 的 image_id。 + try: + created_image_post_id = await self.create_image_post(token, source_image_url) + logger.info( + "ParentPost pre-create media post done: " + f"parent_post_id={parent_post_id}, image_post_id={created_image_post_id}, " + f"media_url={source_image_url}" + ) + except Exception as e: + logger.warning( + "ParentPost pre-create media post failed, continue anyway: " + f"parent_post_id={parent_post_id}, media_url={source_image_url}, error={e}" + ) + + message = self._build_video_message( + prompt=prompt, + preset=preset, + source_image_url=source_image_url, + ) + model_config_override = { + "modelMap": { + "videoGenModelConfig": { + "aspectRatio": aspect_ratio, + "parentPostId": parent_post_id, + "resolutionName": resolution, + "videoLength": video_length, + "isVideoEdit": False, + } + } + } + moderated_max_retry = max(1, int(get_config("video.moderated_max_retry", 5))) + + logger.info( + "ParentPost video request prepared: " + f"token={token_tag}, parent_post_id={parent_post_id}, " + f"message_len={len(message)}, has_prompt={bool((prompt or '').strip())}, " + f"resolution={resolution}, video_length={video_length}, ratio={aspect_ratio}, mode={mode}" + ) + + async def _stream(): + for attempt in range(1, moderated_max_retry + 1): + session = AsyncSession() + moderated_hit = False + try: + async with _get_video_semaphore(): + stream_response = await AppChatReverse.request( + session, + token, + message=message, + model="grok-3", + tool_overrides={"videoGen": True}, + model_config_override=model_config_override, + ) + logger.info( + "Video generation started by parentPostId: " + f"token={token_tag}, parent_post_id={parent_post_id}, attempt={attempt}/{moderated_max_retry}" + ) + async for line in stream_response: + if self._is_moderated_line(line): + moderated_hit = True + logger.warning( + f"Video generation moderated: token={token_tag}, retry {attempt}/{moderated_max_retry}" + ) + break + yield line + + if not moderated_hit: + return + if attempt < moderated_max_retry: + await asyncio.sleep(1.2) + continue + raise UpstreamException( + "Video blocked by moderation", + status_code=400, + details={"moderated": True, "attempts": moderated_max_retry}, + ) + except Exception as e: + logger.error(f"Video generation error: {e}") + if isinstance(e, AppException): + raise + raise UpstreamException(f"Video generation error: {str(e)}") + finally: + try: + await session.close() + except Exception: + pass return _stream() @@ -227,6 +519,9 @@ class VideoService: video_length: int = 6, resolution: str = "480p", preset: str = "normal", + parent_post_id: str | None = None, + source_image_url: str | None = None, + preferred_token: str | None = None, ): """Video generation entrypoint.""" # Get token via intelligent routing. @@ -247,37 +542,60 @@ class VideoService: from app.services.grok.utils.upload import UploadService prompt, file_attachments, image_attachments = MessageExtractor.extract(messages) + parent_post_id = (parent_post_id or "").strip() or None + source_image_url = (source_image_url or "").strip() + preferred_token = (preferred_token or "").strip() + if preferred_token.startswith("sso="): + preferred_token = preferred_token[4:] + used_tokens: set[str] = set() for attempt in range(max_token_retries): - # Select token based on video requirements and pool candidates. - pool_candidates = ModelService.pool_candidates_for_model(model) - token_info = token_mgr.get_token_for_video( - resolution=resolution, - video_length=video_length, - pool_candidates=pool_candidates, - ) + token = "" + if preferred_token and preferred_token not in used_tokens: + if token_mgr.get_pool_name_for_token(preferred_token): + token = preferred_token + logger.info( + f"Video token routing: preferred bound token -> " + f"token={_token_tag(token)}" + ) + else: + used_tokens.add(preferred_token) + logger.warning( + f"Video token routing: preferred token not in pool, fallback to normal routing " + f"(token={_token_tag(preferred_token)})" + ) - if not token_info: - if last_error: - raise last_error - raise AppException( - message="No available tokens. Please try again later.", - error_type=ErrorType.RATE_LIMIT.value, - code="rate_limit_exceeded", - status_code=429, + if not token: + # Select token based on video requirements and pool candidates. + pool_candidates = ModelService.pool_candidates_for_model(model) + token_info = token_mgr.get_token_for_video( + resolution=resolution, + video_length=video_length, + pool_candidates=pool_candidates, + exclude=used_tokens, ) - # Extract token string from TokenInfo. - token = token_info.token - if token.startswith("sso="): - token = token[4:] - pool_name = token_mgr.get_pool_name_for_token(token) - should_upscale = resolution == "720p" and pool_name == BASIC_POOL_NAME + if not token_info: + if last_error: + raise last_error + raise AppException( + message="No available tokens. Please try again later.", + error_type=ErrorType.RATE_LIMIT.value, + code="rate_limit_exceeded", + status_code=429, + ) + + token = token_info.token + if token.startswith("sso="): + token = token[4:] + + used_tokens.add(token) + should_upscale = bool(get_config("video.auto_upscale", True)) try: # Handle image attachments. image_url = None - if image_attachments: + if (not parent_post_id) and image_attachments: upload_service = UploadService() try: for attach_data in image_attachments: @@ -292,7 +610,18 @@ class VideoService: # Generate video. service = VideoService() - if image_url: + if parent_post_id: + response = await service.generate_from_parent_post( + token=token, + prompt=prompt, + parent_post_id=parent_post_id, + source_image_url=source_image_url, + aspect_ratio=aspect_ratio, + video_length=video_length, + resolution=resolution, + preset=preset, + ) + elif image_url: response = await service.generate_from_image( token, prompt, @@ -347,7 +676,7 @@ class VideoService: if rate_limited(e): await token_mgr.mark_rate_limited(token) logger.warning( - f"Token {token[:10]}... rate limited (429), " + f"Token {_token_tag(token)} rate limited (429), " f"trying next token (attempt {attempt + 1}/{max_token_retries})" ) continue @@ -596,10 +925,83 @@ class VideoCollectProcessor(BaseProcessor): logger.warning(f"Video upscale failed: {e}") return video_url + async def _resolve_video_asset_path(self, asset_id: str) -> tuple[str, str]: + """当流里未返回 videoUrl 时,尝试从 assets 接口反查 key。""" + if not asset_id or not self.token: + return "", "" + + retries = 3 + delay = 1.5 + page_size = 50 + max_pages = 20 + marker = f"/{asset_id}/" + + async with AsyncSession() as session: + for attempt in range(1, retries + 1): + params = { + "pageSize": page_size, + "orderBy": "ORDER_BY_LAST_USE_TIME", + "source": "SOURCE_ANY", + "isLatest": "true", + } + page_token = "" + page_count = 0 + try: + while True: + if page_token: + params["pageToken"] = page_token + else: + params.pop("pageToken", None) + + response = await AssetsListReverse.request( + session, self.token, params + ) + data = response.json() if response is not None else {} + assets = data.get("assets", []) if isinstance(data, dict) else [] + + for asset in assets: + if not isinstance(asset, dict): + continue + current_asset_id = str(asset.get("assetId", "")).strip() + key = str(asset.get("key", "")).strip() + mime_type = str(asset.get("mimeType", "")).lower() + if ( + current_asset_id == asset_id + or marker in key + or key.endswith(f"{asset_id}/content") + ): + if mime_type.startswith("video/") or "generated_video" in key: + preview_key = str(asset.get("previewImageKey", "")).strip() + if not preview_key: + aux = asset.get("auxKeys") or {} + if isinstance(aux, dict): + preview_key = str(aux.get("preview-image", "")).strip() + logger.info( + "Video asset resolved by assets list: " + f"asset_id={asset_id}, key={key}, preview={preview_key}" + ) + return key, preview_key + + page_token = str(data.get("nextPageToken", "")).strip() + page_count += 1 + if not page_token or page_count >= max_pages: + break + except Exception as e: + logger.warning( + f"Video asset resolve failed (attempt={attempt}/{retries}): {e}" + ) + + if attempt < retries: + await asyncio.sleep(delay) + + return "", "" + async def process(self, response: AsyncIterable[bytes]) -> dict[str, Any]: """Process and collect video response.""" response_id = "" content = "" + fallback_video_id = "" + fallback_thumb = "" idle_timeout = get_config("video.stream_timeout") try: @@ -615,6 +1017,18 @@ class VideoCollectProcessor(BaseProcessor): resp = data.get("result", {}).get("response", {}) if video_resp := resp.get("streamingVideoGenerationResponse"): + fallback_video_id = ( + str(video_resp.get("videoPostId", "")).strip() + or str(video_resp.get("assetId", "")).strip() + or str(video_resp.get("videoId", "")).strip() + or fallback_video_id + ) + thumb_from_stream = str( + video_resp.get("thumbnailImageUrl", "") + ).strip() + if thumb_from_stream: + fallback_thumb = thumb_from_stream + if video_resp.get("progress") == 100: response_id = resp.get("responseId", "") video_url = video_resp.get("videoUrl", "") @@ -628,6 +1042,14 @@ class VideoCollectProcessor(BaseProcessor): video_url, self.token, thumbnail_url ) logger.info(f"Video generated: {video_url}") + elif model_resp := resp.get("modelResponse"): + file_attachments = model_resp.get("fileAttachments", []) + if isinstance(file_attachments, list): + for fid in file_attachments: + fid = str(fid).strip() + if fid: + fallback_video_id = fid + break except asyncio.CancelledError: logger.debug( @@ -647,6 +1069,23 @@ class VideoCollectProcessor(BaseProcessor): logger.error( f"Video collect request error: {e}", extra={"model": self.model} ) + except UpstreamException as e: + # 对于上游明确返回的业务终止错误(如 moderation 封禁), + # 不应吞掉并伪装成“空结果”,否则上层会误判为 parentPost 空结果继续下一轮。 + details = getattr(e, "details", {}) or {} + is_moderated_block = bool(details.get("moderated")) or ( + "blocked by moderation" in str(e).lower() + ) + if is_moderated_block: + logger.error( + f"Video collect got terminal moderation error: {e}", + extra={"model": self.model}, + ) + raise + logger.error( + f"Video collect upstream error: {e}", + extra={"model": self.model, "error_type": type(e).__name__}, + ) except Exception as e: logger.error( f"Video collect processing error: {e}", @@ -655,6 +1094,23 @@ class VideoCollectProcessor(BaseProcessor): finally: await self.close() + if not content and fallback_video_id: + asset_video_path, asset_thumb_path = await self._resolve_video_asset_path( + fallback_video_id + ) + if asset_video_path: + if self.upscale_on_finish: + asset_video_path = await self._upscale_video_url(asset_video_path) + dl_service = self._get_dl() + content = await dl_service.render_video( + asset_video_path, self.token, asset_thumb_path or fallback_thumb + ) + response_id = response_id or f"chatcmpl-{uuid.uuid4().hex[:24]}" + logger.info( + "Video generated via assets fallback: " + f"video_id={fallback_video_id}, key={asset_video_path}" + ) + return { "id": response_id, "object": "chat.completion", diff --git a/app/services/reverse/assets_upload.py b/app/services/reverse/assets_upload.py index b9d9673..855db30 100644 --- a/app/services/reverse/assets_upload.py +++ b/app/services/reverse/assets_upload.py @@ -55,6 +55,10 @@ class AssetsUploadReverse: "fileMimeType": fileMimeType, "content": content, } + logger.info( + "AssetsUpload request prepared: " + f"fileName={fileName}, fileMimeType={fileMimeType}, content_len={len(content or '')}" + ) # Curl Config timeout = get_config("asset.upload_timeout") diff --git a/app/services/reverse/media_post.py b/app/services/reverse/media_post.py index 6e70d53..d50f5e3 100644 --- a/app/services/reverse/media_post.py +++ b/app/services/reverse/media_post.py @@ -2,7 +2,6 @@ Reverse interface: media post create. """ -import orjson from typing import Any from curl_cffi.requests import AsyncSession @@ -14,8 +13,6 @@ from app.services.reverse.utils.headers import build_headers from app.services.reverse.utils.retry import retry_on_status MEDIA_POST_API = "https://grok.com/rest/media/post/create" - - class MediaPostReverse: """/rest/media/post/create reverse interface.""" @@ -57,6 +54,10 @@ class MediaPostReverse: payload["mediaUrl"] = mediaUrl if prompt: payload["prompt"] = prompt + logger.info( + "MediaPost request prepared: " + f"mediaType={mediaType}, has_media_url={bool(mediaUrl)}, prompt_len={len(prompt or '')}" + ) # Curl Config timeout = get_config("video.timeout") @@ -66,7 +67,7 @@ class MediaPostReverse: response = await session.post( MEDIA_POST_API, headers=headers, - data=orjson.dumps(payload), + json=payload, timeout=timeout, proxies=proxies, impersonate=browser, diff --git a/app/services/reverse/video_upscale.py b/app/services/reverse/video_upscale.py index f6c70e1..0ef606d 100644 --- a/app/services/reverse/video_upscale.py +++ b/app/services/reverse/video_upscale.py @@ -2,7 +2,6 @@ Reverse interface: video upscale. """ -import orjson from typing import Any from curl_cffi.requests import AsyncSession @@ -14,8 +13,6 @@ from app.services.reverse.utils.headers import build_headers from app.services.reverse.utils.retry import retry_on_status VIDEO_UPSCALE_API = "https://grok.com/rest/media/video/upscale" - - class VideoUpscaleReverse: """/rest/media/video/upscale reverse interface.""" @@ -46,6 +43,7 @@ class VideoUpscaleReverse: # Build payload payload = {"videoId": video_id} + logger.info(f"VideoUpscale request prepared: video_id={video_id}") # Curl Config timeout = get_config("video.timeout") @@ -55,7 +53,7 @@ class VideoUpscaleReverse: response = await session.post( VIDEO_UPSCALE_API, headers=headers, - data=orjson.dumps(payload), + json=payload, timeout=timeout, proxies=proxies, impersonate=browser, diff --git a/app/services/reverse/ws_imagine.py b/app/services/reverse/ws_imagine.py index e9e648a..7b44308 100644 --- a/app/services/reverse/ws_imagine.py +++ b/app/services/reverse/ws_imagine.py @@ -86,6 +86,7 @@ class ImagineWebSocketReverse: "skip_upsampler": False, "is_initial": False, "aspect_ratio": aspect_ratio, + "aspectRatio": aspect_ratio, }, } ], @@ -180,6 +181,10 @@ class ImagineWebSocketReverse: message = self._build_request_message( request_id, prompt, aspect_ratio, enable_nsfw ) + logger.info( + "Imagine WS request prepared: " + f"request_id={request_id}, ratio={aspect_ratio}, nsfw={enable_nsfw}" + ) await ws.send_json(message) logger.info(f"WebSocket request sent: {prompt[:80]}...") diff --git a/app/services/token/manager.py b/app/services/token/manager.py index 1530cbe..b9868f9 100644 --- a/app/services/token/manager.py +++ b/app/services/token/manager.py @@ -38,6 +38,15 @@ def _default_quota_for_pool(pool_name: str) -> int: return BASIC__DEFAULT_QUOTA +def _token_tag(token: str) -> str: + raw = token[4:] if token.startswith("sso=") else token + if not raw: + return "empty" + if len(raw) <= 14: + return raw + return f"{raw[:6]}...{raw[-6:]}" + + class TokenManager: """管理 Token 的增删改查和配额同步""" @@ -211,7 +220,9 @@ class TokenManager: return token[4:] return token - def get_token_info(self, pool_name: str = "ssoBasic") -> Optional["TokenInfo"]: + def get_token_info( + self, pool_name: str = "ssoBasic", exclude: set | None = None + ) -> Optional["TokenInfo"]: """ 获取可用 Token 的完整信息 @@ -226,7 +237,7 @@ class TokenManager: logger.warning(f"Pool '{pool_name}' not found") return None - token_info = pool.select() + token_info = pool.select(exclude=exclude) if not token_info: logger.warning(f"No available token in pool '{pool_name}'") return None @@ -238,6 +249,7 @@ class TokenManager: resolution: str = "480p", video_length: int = 6, pool_candidates: Optional[List[str]] = None, + exclude: Optional[set[str]] = None, ) -> Optional["TokenInfo"]: """ 根据视频需求智能选择 Token 池 @@ -269,17 +281,17 @@ class TokenManager: ordered_pools = [primary_pool, fallback_pool] for idx, pool_name in enumerate(ordered_pools): - token_info = self.get_token_info(pool_name) + token_info = self.get_token_info(pool_name, exclude=exclude) if token_info: if idx == 0: logger.info( f"Video token routing: resolution={resolution}, length={video_length}s -> " - f"pool={pool_name} (token={token_info.token[:10]}...)" + f"pool={pool_name} (token={_token_tag(token_info.token)})" ) else: logger.info( f"Video token routing: fallback from {ordered_pools[0]} -> {pool_name} " - f"(token={token_info.token[:10]}...)" + f"(token={_token_tag(token_info.token)})" ) return token_info diff --git a/app/static/admin/css/cache.css b/app/static/admin/css/cache.css index 2b37295..b19e6ec 100644 --- a/app/static/admin/css/cache.css +++ b/app/static/admin/css/cache.css @@ -302,3 +302,43 @@ box-shadow: 0 0 0 2px #000; border-color: #000; } + +@media (max-width: 900px) { + #batch-actions { + left: 12px; + right: 12px; + bottom: 12px; + transform: none; + border-radius: 14px; + padding: 10px; + white-space: normal; + gap: 8px; + } + + #batch-actions .toolbar-sep { + display: none; + } + + #batch-actions .batch-action-buttons { + width: 100%; + display: flex; + flex-wrap: wrap; + gap: 8px; + } + + #batch-actions .batch-action-buttons .geist-button-outline, + #batch-actions .batch-action-buttons .geist-button-danger { + flex: 1; + min-width: 120px; + justify-content: center; + } + + #batch-actions #batch-progress { + width: 100%; + flex-wrap: wrap; + } + + .failure-list { + max-height: min(46dvh, 320px); + } +} diff --git a/app/static/admin/css/config.css b/app/static/admin/css/config.css index 8fa851b..ca65dd2 100644 --- a/app/static/admin/css/config.css +++ b/app/static/admin/css/config.css @@ -57,3 +57,22 @@ top: 50%; transform: translateY(-50%); } + +@media (max-width: 900px) { + .config-section { + padding: 14px; + border-radius: 10px; + } + + .config-field.has-action { + padding-right: 0; + padding-bottom: 40px; + } + + .config-field-action { + top: auto; + right: 0; + bottom: 0; + transform: none; + } +} diff --git a/app/static/common/css/login.css b/app/static/common/css/login.css index 119a13a..d7e3d50 100644 --- a/app/static/common/css/login.css +++ b/app/static/common/css/login.css @@ -10,6 +10,12 @@ overflow: hidden; } +@supports (min-height: 100dvh) { + .login-body { + min-height: 100dvh; + } +} + .login-bg { position: fixed; inset: 0; @@ -97,3 +103,20 @@ font-size: 12px; justify-content: center; } + +@media (max-width: 900px) { + .login-shell { + width: min(460px, 100vw); + padding: 16px; + } + + .login-card { + padding: 18px; + } + + .login-input, + .login-button { + height: 42px; + font-size: 16px; + } +} diff --git a/app/static/common/html/header.html b/app/static/common/html/header.html index 2330a99..6eba390 100644 --- a/app/static/common/html/header.html +++ b/app/static/common/html/header.html @@ -1,9 +1,9 @@ diff --git a/app/static/common/js/admin-auth.js b/app/static/common/js/admin-auth.js index 36898d0..1a25f45 100644 --- a/app/static/common/js/admin-auth.js +++ b/app/static/common/js/admin-auth.js @@ -255,18 +255,22 @@ function formatStorageLabel(type) { } async function updateStorageModeButton() { - const btn = document.getElementById('storage-mode-btn'); - if (!btn) return; - btn.textContent = '...'; - btn.title = '存储模式'; - btn.classList.remove('storage-ready'); + const buttons = Array.from(document.querySelectorAll('[data-storage-mode-btn]')); + if (!buttons.length) return; + buttons.forEach((btn) => { + btn.textContent = '...'; + btn.title = '存储模式'; + btn.classList.remove('storage-ready'); + }); const storageType = await fetchStorageType(); const label = formatStorageLabel(storageType); - btn.textContent = label === '-' ? label : label.toUpperCase(); - btn.title = '存储模式'; - if (label !== '-') { - btn.classList.add('storage-ready'); - } + buttons.forEach((btn) => { + btn.textContent = label === '-' ? label : label.toUpperCase(); + btn.title = '存储模式'; + if (label !== '-') { + btn.classList.add('storage-ready'); + } + }); } if (document.readyState === 'loading') { diff --git a/app/static/common/js/header.js b/app/static/common/js/header.js index 27cdae1..593a402 100644 --- a/app/static/common/js/header.js +++ b/app/static/common/js/header.js @@ -2,7 +2,7 @@ async function loadAdminHeader() { const container = document.getElementById('app-header'); if (!container) return; try { - const res = await fetch('/static/common/html/header.html?v=0.3.0'); + const res = await fetch('/static/common/html/header.html?v=0.3.1'); if (!res.ok) return; container.innerHTML = await res.text(); const path = window.location.pathname; @@ -23,6 +23,61 @@ async function loadAdminHeader() { if (typeof updateStorageModeButton === 'function') { updateStorageModeButton(); } + + const nav = container.querySelector('nav'); + const menuToggle = container.querySelector('#admin-menu-toggle'); + const mobileMenu = container.querySelector('#admin-mobile-menu'); + if (menuToggle && mobileMenu) { + const setMenuOpen = (open) => { + const isOpen = Boolean(open); + menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + mobileMenu.hidden = !isOpen; + mobileMenu.classList.toggle('open', isOpen); + if (nav) { + nav.classList.toggle('mobile-menu-open', isOpen); + } + document.body.classList.toggle('mobile-menu-open', isOpen); + }; + + setMenuOpen(false); + + menuToggle.addEventListener('click', () => { + const expanded = menuToggle.getAttribute('aria-expanded') === 'true'; + setMenuOpen(!expanded); + }); + + mobileMenu.addEventListener('click', (event) => { + const target = event.target; + if (!(target instanceof Element)) return; + if (target.closest('a') || target.closest('button')) { + setMenuOpen(false); + } + }); + + document.addEventListener('click', (event) => { + const target = event.target; + if (!(target instanceof Node)) return; + if (!container.contains(target)) { + setMenuOpen(false); + } + }); + + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + setMenuOpen(false); + } + }); + + window.addEventListener('resize', () => { + if (window.innerWidth > 900) { + setMenuOpen(false); + } + }); + } + + if (window.themeController && typeof window.themeController.refreshButtons === 'function') { + window.themeController.refreshButtons(); + } } catch (e) { // Fail silently to avoid breaking page load } diff --git a/app/static/common/js/mobile-fab.js b/app/static/common/js/mobile-fab.js new file mode 100644 index 0000000..3a1e1ee --- /dev/null +++ b/app/static/common/js/mobile-fab.js @@ -0,0 +1,95 @@ +(() => { + function bindFab(root) { + if (!root || root.dataset.fabBound === '1') return; + root.dataset.fabBound = '1'; + + const toggleBtn = root.querySelector('[data-fab-toggle]'); + const proxyButtons = Array.from(root.querySelectorAll('[data-proxy-click]')); + + const setOpen = (open) => { + const isOpen = Boolean(open); + root.classList.toggle('open', isOpen); + if (toggleBtn) { + toggleBtn.setAttribute('aria-expanded', String(isOpen)); + } + }; + + if (toggleBtn) { + toggleBtn.addEventListener('click', () => { + setOpen(!root.classList.contains('open')); + }); + } + + document.addEventListener('click', (event) => { + const target = event.target; + if (!(target instanceof Node)) return; + if (!root.contains(target)) { + setOpen(false); + } + }); + + proxyButtons.forEach((btn) => { + btn.addEventListener('click', () => { + const selector = btn.getAttribute('data-proxy-click'); + if (!selector) return; + const source = document.querySelector(selector); + if (!(source instanceof HTMLElement)) return; + source.click(); + }); + }); + + const syncProxyState = () => { + proxyButtons.forEach((btn) => { + const selector = btn.getAttribute('data-proxy-click'); + if (!selector) return; + const source = document.querySelector(selector); + if (!(source instanceof HTMLElement)) { + btn.classList.add('hidden'); + btn.disabled = true; + return; + } + const logicallyHidden = Boolean(source.hidden) || source.classList.contains('hidden'); + btn.disabled = Boolean(source.disabled); + btn.classList.toggle('hidden', logicallyHidden); + const nextLabel = String(source.textContent || '').trim(); + if (nextLabel && btn.textContent !== nextLabel) { + btn.textContent = nextLabel; + } + }); + }; + + const observer = new MutationObserver(syncProxyState); + const observedSources = new Set(); + proxyButtons.forEach((btn) => { + const selector = btn.getAttribute('data-proxy-click'); + if (!selector) return; + const source = document.querySelector(selector); + if (!(source instanceof HTMLElement) || observedSources.has(source)) return; + observedSources.add(source); + observer.observe(source, { + attributes: true, + attributeFilter: ['class', 'style', 'hidden', 'disabled'], + childList: true, + characterData: true, + subtree: true, + }); + }); + + syncProxyState(); + setOpen(false); + } + + function setupAll() { + document.querySelectorAll('[data-mobile-fab]').forEach((root) => { + if (root instanceof HTMLElement) { + bindFab(root); + } + }); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', setupAll); + } else { + setupAll(); + } +})(); diff --git a/app/static/common/js/parentpost-memory.js b/app/static/common/js/parentpost-memory.js new file mode 100644 index 0000000..4240e47 --- /dev/null +++ b/app/static/common/js/parentpost-memory.js @@ -0,0 +1,138 @@ +(() => { + const STORAGE_KEY = 'grok2api_parent_post_memory_v1'; + const MAX_ITEMS = 300; + + function nowMs() { + return Date.now(); + } + + function isParentPostId(value) { + return /^[0-9a-fA-F-]{32,36}$/.test(String(value || '').trim()); + } + + function buildImaginePublicUrl(parentPostId) { + const id = String(parentPostId || '').trim(); + if (!id) return ''; + return `https://imagine-public.x.ai/imagine-public/images/${id}.jpg`; + } + + function extractParentPostId(text) { + const raw = String(text || '').trim(); + if (!raw) return ''; + if (isParentPostId(raw)) return raw; + + const patterns = [ + /\/generated\/([0-9a-fA-F-]{32,36})(?:\/|$)/, + /\/imagine-public\/images\/([0-9a-fA-F-]{32,36})(?:\.jpg|\/|$)/, + /\/images\/([0-9a-fA-F-]{32,36})(?:\.jpg|\/|$)/, + ]; + for (const pattern of patterns) { + const match = raw.match(pattern); + if (match) { + return match[1]; + } + } + + const all = raw.match(/([0-9a-fA-F-]{32,36})/g); + return all && all.length ? all[all.length - 1] : ''; + } + + function loadMemory() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const data = JSON.parse(raw); + if (!Array.isArray(data)) return []; + return data.filter((item) => item && isParentPostId(item.parentPostId)); + } catch (e) { + return []; + } + } + + function saveMemory(items) { + try { + const normalized = Array.isArray(items) ? items.slice(0, MAX_ITEMS) : []; + localStorage.setItem(STORAGE_KEY, JSON.stringify(normalized)); + } catch (e) { + // ignore + } + } + + function normalizeUrl(value) { + const raw = String(value || '').trim(); + if (!raw) return ''; + if (raw.startsWith('http://') || raw.startsWith('https://') || raw.startsWith('data:image/')) { + return raw; + } + if (raw.startsWith('/')) { + return raw; + } + return raw; + } + + function remember(entry) { + if (!entry || typeof entry !== 'object') return null; + + const parentPostId = extractParentPostId(entry.parentPostId || entry.parent_post_id || entry.id || ''); + if (!isParentPostId(parentPostId)) return null; + + const sourceImageUrl = normalizeUrl(entry.sourceImageUrl || entry.source_image_url || ''); + const imageUrl = normalizeUrl(entry.imageUrl || entry.image_url || entry.url || ''); + const item = { + parentPostId, + sourceImageUrl: sourceImageUrl || imageUrl || buildImaginePublicUrl(parentPostId), + imageUrl: imageUrl || sourceImageUrl || buildImaginePublicUrl(parentPostId), + origin: String(entry.origin || 'unknown'), + updatedAt: Number(entry.updatedAt || nowMs()), + }; + + const all = loadMemory(); + const idx = all.findIndex((it) => it.parentPostId === parentPostId); + if (idx >= 0) { + all.splice(idx, 1); + } + all.unshift(item); + saveMemory(all); + return item; + } + + function getByParentPostId(parentPostId) { + const id = extractParentPostId(parentPostId); + if (!isParentPostId(id)) return null; + const all = loadMemory(); + return all.find((item) => item.parentPostId === id) || null; + } + + function list(limit = 50) { + const n = Math.max(1, Math.min(500, Number(limit) || 50)); + return loadMemory().slice(0, n); + } + + function resolveByText(text) { + const raw = String(text || '').trim(); + const parentPostId = extractParentPostId(raw); + if (!parentPostId) return null; + const hit = getByParentPostId(parentPostId); + if (hit) return hit; + const looksLikeUrl = raw.startsWith('http://') || raw.startsWith('https://') || raw.startsWith('/'); + const fallbackUrl = looksLikeUrl ? raw : buildImaginePublicUrl(parentPostId); + return { + parentPostId, + sourceImageUrl: fallbackUrl, + imageUrl: fallbackUrl, + origin: 'fallback', + updatedAt: nowMs(), + }; + } + + window.ParentPostMemory = { + key: STORAGE_KEY, + extractParentPostId, + isParentPostId, + buildImaginePublicUrl, + remember, + getByParentPostId, + list, + resolveByText, + }; +})(); diff --git a/app/static/common/js/prompt-enhancer.js b/app/static/common/js/prompt-enhancer.js index 5704fbf..e3694c5 100644 --- a/app/static/common/js/prompt-enhancer.js +++ b/app/static/common/js/prompt-enhancer.js @@ -1,5 +1,6 @@ (() => { const enhanceStateMap = new WeakMap(); + const enhanceRequestMap = new WeakMap(); function toast(message, type) { if (typeof window.showToast === 'function') { @@ -15,16 +16,47 @@ .prompt-enhance-wrap { position: relative; width: 100%; + --pe-action-width: 104px; + --pe-action-height: 30px; + --pe-action-radius: 8px; + --pe-slash-size: 8px; + --pe-lang-share: 40%; + --pe-border-color: #5f7087; + --pe-scrollbar-thumb: rgba(110, 132, 160, 0.82); + --pe-scrollbar-thumb-hover: rgba(86, 112, 145, 0.95); + --pe-scrollbar-track: rgba(148, 163, 184, 0.18); } .prompt-enhance-wrap > textarea { padding-bottom: 40px; + scrollbar-width: thin; + scrollbar-color: var(--pe-scrollbar-thumb) var(--pe-scrollbar-track); + } + .prompt-enhance-wrap > textarea::-webkit-scrollbar { + width: 7px; + height: 7px; + } + .prompt-enhance-wrap > textarea::-webkit-scrollbar-track { + background: var(--pe-scrollbar-track); + border-radius: 999px; + } + .prompt-enhance-wrap > textarea::-webkit-scrollbar-thumb { + background: var(--pe-scrollbar-thumb); + border-radius: 999px; + border: 1px solid transparent; + background-clip: padding-box; + } + .prompt-enhance-wrap > textarea:hover::-webkit-scrollbar-thumb { + background: var(--pe-scrollbar-thumb-hover); + } + .prompt-enhance-wrap > textarea:focus::-webkit-scrollbar-thumb { + background: var(--pe-scrollbar-thumb-hover); } .prompt-enhance-wrap.lightbox-mode > textarea { width: 100%; display: block; box-sizing: border-box; padding-left: 12px; - padding-right: 124px; + padding-right: calc(var(--pe-action-width) + 20px); min-height: 84px; } .prompt-enhance-wrap.lightbox-mode { @@ -33,6 +65,17 @@ width: 100%; position: relative; } + .prompt-enhance-wrap > .prompt-enhance-actions { + position: absolute; + right: 10px; + bottom: 10px; + z-index: 3; + width: var(--pe-action-width); + height: var(--pe-action-height); + display: flex; + align-items: stretch; + justify-content: stretch; + } .prompt-enhance-wrap.lightbox-mode > .inline-send-inside, .prompt-enhance-wrap.lightbox-mode > #lightboxEditSend, .prompt-enhance-wrap.lightbox-mode > .lightbox-edit-send { @@ -48,21 +91,21 @@ border-radius: 8px; flex: none !important; } - .prompt-enhance-wrap.lightbox-mode > .prompt-enhance-btn { - position: absolute !important; + .prompt-enhance-wrap.lightbox-mode > .prompt-enhance-actions { right: 10px !important; bottom: 46px !important; z-index: 4; - height: 30px !important; - min-width: 104px; - width: auto !important; - margin: 0 !important; - padding: 0 12px !important; } @media (max-width: 768px) { + .prompt-enhance-wrap { + --pe-action-width: 92px; + --pe-action-height: 28px; + --pe-slash-size: 7px; + --pe-lang-share: 40%; + } .prompt-enhance-wrap.lightbox-mode > textarea { padding-left: 12px; - padding-right: 104px; + padding-right: calc(var(--pe-action-width) + 12px); padding-bottom: 50px; min-height: 96px; } @@ -75,74 +118,120 @@ height: 28px !important; padding: 0 10px !important; } - .prompt-enhance-wrap.lightbox-mode > .prompt-enhance-btn { + .prompt-enhance-wrap.lightbox-mode > .prompt-enhance-actions { right: 8px !important; bottom: 42px !important; - min-width: 92px; - height: 28px !important; - padding: 0 10px !important; } } - .prompt-enhance-btn { - position: absolute; - right: 10px; - bottom: 10px; - z-index: 3; - height: 30px; - min-width: 92px; + .prompt-enhance-actions > .prompt-enhance-btn, + .prompt-enhance-actions > .prompt-lang-toggle-btn { + height: 100%; + min-width: 0; + width: 100%; padding: 0 10px; - border-radius: 8px; background: var(--bg); - border-color: var(--border); + border-color: var(--pe-border-color); color: var(--fg); cursor: pointer; user-select: none; + transition: border-color .15s ease, background .15s ease; + } + .prompt-enhance-actions > .prompt-enhance-btn { + border-radius: var(--pe-action-radius); + } + .prompt-enhance-actions > .prompt-lang-toggle-btn { + display: none; } .prompt-enhance-btn:hover { - border-color: #000; + border-color: #34455d; + background: #e7eef8; + box-shadow: inset 0 0 0 1px rgba(52, 69, 93, 0.22); + } + .prompt-enhance-btn:active { + border-color: #2a3d57; + background: #dce7f6; } html[data-theme='dark'] .prompt-enhance-btn { background: #111821; - border-color: #3b4654; + border-color: #607286; color: var(--fg); } html[data-theme='dark'] .prompt-enhance-btn:hover { - border-color: #6b7788; + border-color: #7a8ea6; background: #1a2330; } - .prompt-lang-toggle-btn { - position: absolute; - right: 110px; - bottom: 10px; - z-index: 3; - height: 30px; - min-width: 48px; - padding: 0 10px; - border-radius: 8px; - background: var(--bg); - border-color: var(--border); - color: var(--fg); - cursor: pointer; - user-select: none; - display: none; - } - .prompt-lang-toggle-btn.is-visible { - display: inline-flex; - align-items: center; - justify-content: center; - } .prompt-lang-toggle-btn:hover { - border-color: #000; + border-color: #34455d; + background: #e7eef8; + box-shadow: inset 0 0 0 1px rgba(52, 69, 93, 0.22); + } + .prompt-lang-toggle-btn:active { + border-color: #2a3d57; + background: #dce7f6; } html[data-theme='dark'] .prompt-lang-toggle-btn { background: #111821; - border-color: #3b4654; + border-color: #607286; color: var(--fg); } html[data-theme='dark'] .prompt-lang-toggle-btn:hover { - border-color: #6b7788; + border-color: #7a8ea6; background: #1a2330; } + .prompt-enhance-actions.is-group-active { + padding: 1px; + background: var(--pe-border-color); + border: 1px solid var(--pe-border-color); + border-radius: var(--pe-action-radius); + overflow: hidden; + } + .prompt-enhance-actions.is-group-active > .prompt-lang-toggle-btn.is-visible { + display: inline-flex; + align-items: center; + justify-content: center; + flex: 0 0 var(--pe-lang-share); + max-width: none; + margin-right: 0; + border-radius: var(--pe-action-radius) 0 0 var(--pe-action-radius); + border: 0; + clip-path: polygon(0 0, 100% 0, calc(100% - var(--pe-slash-size)) 100%, 0 100%); + padding: 0 4px 0 8px; + z-index: 2; + } + .prompt-enhance-actions.is-group-active > .prompt-enhance-btn { + flex: 1 1 auto; + min-width: 0; + max-width: none; + margin-left: calc(var(--pe-slash-size) * -1); + border-radius: 0 var(--pe-action-radius) var(--pe-action-radius) 0; + border: 0; + clip-path: polygon(var(--pe-slash-size) 0, 100% 0, 100% 100%, 0 100%); + padding: 0 10px 0 calc(10px + var(--pe-slash-size)); + z-index: 1; + } + .prompt-enhance-actions.is-group-active > .prompt-lang-toggle-btn.is-visible:hover, + .prompt-enhance-actions.is-group-active > .prompt-enhance-btn:hover { + border: 0; + background: #dfe9f7; + box-shadow: none; + } + .prompt-enhance-actions.is-group-active > .prompt-lang-toggle-btn.is-visible:active, + .prompt-enhance-actions.is-group-active > .prompt-enhance-btn:active { + border: 0; + background: #d3e0f4; + box-shadow: none; + } + html[data-theme='dark'] .prompt-enhance-actions.is-group-active > .prompt-lang-toggle-btn.is-visible:hover, + html[data-theme='dark'] .prompt-enhance-actions.is-group-active > .prompt-enhance-btn:hover { + border: 0; + background: #1d2a3a; + } + html[data-theme='dark'] .prompt-enhance-wrap { + --pe-border-color: #607286; + --pe-scrollbar-thumb: rgba(130, 154, 184, 0.72); + --pe-scrollbar-thumb-hover: rgba(156, 182, 214, 0.9); + --pe-scrollbar-track: rgba(86, 102, 123, 0.28); + } .prompt-enhance-btn:disabled { opacity: 0.5; cursor: not-allowed; @@ -166,18 +255,11 @@ return false; } - async function callEnhanceApi(rawPrompt) { - if (typeof window.ensurePublicKey !== 'function' || typeof window.buildAuthHeaders !== 'function') { - throw new Error('public_auth_api_missing'); - } - const authHeader = await window.ensurePublicKey(); - if (authHeader === null) { - throw new Error('public_key_missing'); - } - + async function callEnhanceApi(rawPrompt, signal, authHeader, requestId) { const body = { prompt: rawPrompt, temperature: 0.7, + request_id: requestId, }; const res = await fetch('/v1/public/prompt/enhance', { @@ -185,8 +267,10 @@ headers: { 'Content-Type': 'application/json', ...window.buildAuthHeaders(authHeader), + 'X-Enhance-Request-Id': requestId, }, body: JSON.stringify(body), + signal, }); if (!res.ok) { let detail = ''; @@ -206,16 +290,35 @@ return text; } + function newRequestId() { + if (window.crypto && typeof window.crypto.randomUUID === 'function') { + return window.crypto.randomUUID(); + } + return `enh_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`; + } + + function requestEnhanceStop(meta) { + if (!meta || !meta.requestId || !meta.authHeader) return; + fetch('/v1/public/prompt/enhance/stop', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...window.buildAuthHeaders(meta.authHeader), + }, + body: JSON.stringify({ request_id: meta.requestId }), + }).catch(() => {}); + } + function isMobileViewport() { return window.matchMedia('(max-width: 768px)').matches; } function parseEnhancedPrompt(text) { const raw = String(text || '').trim(); - const headMatch = raw.match(/^([\s\S]*?)\s*最终提示词:/); - const enMatch = raw.match(/最终提示词:\s*([\s\S]*?)\s*中文参考版:/); - const zhMatch = raw.match(/中文参考版:\s*([\s\S]*?)\s*可调参数:/); - const tailMatch = raw.match(/(可调参数:[\s\S]*)$/); + const headMatch = raw.match(/^([\s\S]*?)\s*最终提示词[::]/); + const enMatch = raw.match(/最终提示词[::]\s*([\s\S]*?)\s*中文参考版[::]/); + const zhMatch = raw.match(/中文参考版[::]\s*([\s\S]*?)\s*可调参数[::]/); + const tailMatch = raw.match(/(可调参数[::][\s\S]*)$/); return { head: headMatch && headMatch[1] ? String(headMatch[1]).trim() : '', en: enMatch && enMatch[1] ? String(enMatch[1]).trim() : '', @@ -232,7 +335,7 @@ } function updateToggleButtonText(toggleBtn, mode) { - toggleBtn.textContent = mode === 'zh' ? '中文' : 'EN'; + toggleBtn.textContent = mode === 'en' ? 'CN' : 'EN'; } function setToggleButtonVisible(toggleBtn, visible) { @@ -244,11 +347,118 @@ enhanceBtn.textContent = mode === 'clear' ? '清空' : '增强提示词'; } - function resetEnhancerState(textarea, enhanceBtn, toggleBtn) { + function pickReferenceActionButton(textarea, wrapper) { + if (!wrapper) return null; + const inlineBtn = wrapper.querySelector('.inline-submit-btn, .inline-send-inside, #lightboxEditSend, .lightbox-edit-send'); + if (inlineBtn instanceof HTMLButtonElement) return inlineBtn; + + const id = String(textarea && textarea.id ? textarea.id : '').toLowerCase(); + if (id === 'editpromptinput') { + const btn = document.getElementById('submitEditBtn'); + if ( + btn instanceof HTMLButtonElement && + btn.classList.contains('inline-submit-btn') && + wrapper.contains(btn) + ) { + return btn; + } + } + if (id === 'lightboxeditinput') { + const btn = document.getElementById('lightboxEditSend'); + if ( + btn instanceof HTMLButtonElement && + btn.classList.contains('inline-send-inside') && + wrapper.contains(btn) + ) { + return btn; + } + } + return null; + } + + function syncActionDimensions(textarea, wrapper, actionWrap) { + if (!textarea || !wrapper || !actionWrap) return null; + const refBtn = pickReferenceActionButton(textarea, wrapper); + const mobile = isMobileViewport(); + const targetWidth = mobile ? 92 : 104; + const targetHeight = mobile ? 28 : 30; + wrapper.style.setProperty('--pe-action-width', `${targetWidth}px`); + wrapper.style.setProperty('--pe-action-height', `${targetHeight}px`); + if (!(refBtn instanceof HTMLButtonElement)) return null; + const style = window.getComputedStyle(refBtn); + const radius = parseFloat(style.borderTopLeftRadius || '8') || 8; + wrapper.style.setProperty('--pe-action-radius', `${Math.max(4, Math.round(radius))}px`); + return refBtn; + } + + function bindActionDimensionSync(textarea, wrapper, actionWrap) { + const run = () => syncActionDimensions(textarea, wrapper, actionWrap); + let observedBtn = null; + let resizeObserver = null; + let mutationObserver = null; + + const rebind = () => { + const nextBtn = run(); + if (!window.ResizeObserver) return; + if (!resizeObserver) { + resizeObserver = new ResizeObserver(() => run()); + } + if (observedBtn === nextBtn) return; + if (observedBtn) resizeObserver.unobserve(observedBtn); + observedBtn = nextBtn; + if (observedBtn) resizeObserver.observe(observedBtn); + }; + + rebind(); + requestAnimationFrame(rebind); + setTimeout(rebind, 80); + setTimeout(rebind, 240); + + window.addEventListener('resize', rebind, { passive: true }); + mutationObserver = new MutationObserver(() => rebind()); + mutationObserver.observe(wrapper, { childList: true }); + wrapper.__peResizeObserver = resizeObserver; + wrapper.__peMutationObserver = mutationObserver; + } + + function syncActionGroupState(enhanceBtn, toggleBtn, actionWrap) { + if (!actionWrap || !enhanceBtn || !toggleBtn) return; + const isClearMode = String(enhanceBtn.dataset.mode || 'enhance') === 'clear'; + const isRunning = String(enhanceBtn.dataset.running || '0') === '1'; + const langVisible = toggleBtn.classList.contains('is-visible'); + actionWrap.classList.toggle('is-group-active', isClearMode && langVisible && !isRunning); + } + + function setEnhanceRunning(textarea, enhanceBtn, toggleBtn, running, actionWrap) { + enhanceBtn.dataset.running = running ? '1' : '0'; + if (running) { + enhanceBtn.textContent = '中止'; + enhanceBtn.disabled = false; + if (toggleBtn) toggleBtn.disabled = true; + syncActionGroupState(enhanceBtn, toggleBtn, actionWrap); + return; + } + if (toggleBtn) toggleBtn.disabled = false; + setEnhanceButtonMode(enhanceBtn, String(enhanceBtn.dataset.mode || 'enhance')); + enhanceRequestMap.delete(textarea); + syncActionGroupState(enhanceBtn, toggleBtn, actionWrap); + } + + function cancelEnhance(textarea) { + const requestMeta = enhanceRequestMap.get(textarea); + if (!requestMeta) return; + if (requestMeta.controller) { + requestMeta.controller.abort(); + } + requestEnhanceStop(requestMeta); + } + + function resetEnhancerState(textarea, enhanceBtn, toggleBtn, actionWrap) { enhanceStateMap.delete(textarea); setToggleButtonVisible(toggleBtn, false); updateToggleButtonText(toggleBtn, 'zh'); setEnhanceButtonMode(enhanceBtn, 'enhance'); + syncActionGroupState(enhanceBtn, toggleBtn, actionWrap); } function buildDesktopText(state, mode) { @@ -282,25 +492,44 @@ updateToggleButtonText(toggleBtn, state.mode); } - async function onEnhanceClick(textarea, enhanceBtn, toggleBtn) { + async function onEnhanceClick(textarea, enhanceBtn, toggleBtn, actionWrap) { + if (String(enhanceBtn.dataset.running || '0') === '1') { + cancelEnhance(textarea); + toast('已取消提示词增强', 'warning'); + return; + } const currentMode = String(enhanceBtn.dataset.mode || 'enhance'); if (currentMode === 'clear') { applyPromptToTextarea(textarea, ''); - resetEnhancerState(textarea, enhanceBtn, toggleBtn); + resetEnhancerState(textarea, enhanceBtn, toggleBtn, actionWrap); toast('已清空提示词', 'success'); return; } const raw = String(textarea.value || '').trim(); if (!raw) { - toast('请先输入提示词', 'warning'); + toast('请输入提示词', 'warning'); return; } - enhanceBtn.disabled = true; - toggleBtn.disabled = true; - enhanceBtn.textContent = '增强中...'; + if (typeof window.ensurePublicKey !== 'function' || typeof window.buildAuthHeaders !== 'function') { + toast('公共鉴权脚本未加载', 'error'); + return; + } + const authHeader = await window.ensurePublicKey(); + if (authHeader === null) { + toast('请先配置 Public Key', 'error'); + return; + } + const requestId = newRequestId(); + const controller = new AbortController(); + enhanceRequestMap.set(textarea, { + controller, + requestId, + authHeader, + }); + setEnhanceRunning(textarea, enhanceBtn, toggleBtn, true, actionWrap); try { - const enhanced = await callEnhanceApi(raw); + const enhanced = await callEnhanceApi(raw, controller.signal, authHeader, requestId); const parsed = parseEnhancedPrompt(enhanced); const hasDualLanguage = Boolean(parsed.en && parsed.zh && parsed.head && parsed.tail); const mode = ((enhanceStateMap.get(textarea) || {}).mode || 'zh'); @@ -322,18 +551,17 @@ applyPromptToTextarea(textarea, parsed.raw); } setEnhanceButtonMode(enhanceBtn, 'clear'); + syncActionGroupState(enhanceBtn, toggleBtn, actionWrap); toast('提示词增强完成', 'success'); } catch (e) { - const msg = String(e && e.message ? e.message : e); - if (msg === 'public_key_missing') { - toast('请先配置 Public Key', 'error'); - } else { - toast(`提示词增强失败: ${msg}`, 'error'); + if (e && e.name === 'AbortError') { + toast('已取消提示词增强', 'warning'); + return; } + const msg = String(e && e.message ? e.message : e); + toast(`提示词增强失败: ${msg}`, 'error'); } finally { - enhanceBtn.disabled = false; - toggleBtn.disabled = false; - setEnhanceButtonMode(enhanceBtn, String(enhanceBtn.dataset.mode || 'enhance')); + setEnhanceRunning(textarea, enhanceBtn, toggleBtn, false, actionWrap); } } @@ -376,6 +604,10 @@ } } + const actionWrap = document.createElement('div'); + actionWrap.className = 'prompt-enhance-actions'; + wrapper.appendChild(actionWrap); + const langBtn = document.createElement('button'); langBtn.type = 'button'; langBtn.className = 'geist-button-outline prompt-lang-toggle-btn'; @@ -389,14 +621,16 @@ const nextMode = (state.mode || 'zh') === 'zh' ? 'en' : 'zh'; applyEnhancedByMode(textarea, langBtn, nextMode); }); - wrapper.appendChild(langBtn); + actionWrap.appendChild(langBtn); const button = document.createElement('button'); button.type = 'button'; button.className = 'geist-button-outline prompt-enhance-btn'; setEnhanceButtonMode(button, 'enhance'); - button.addEventListener('click', () => onEnhanceClick(textarea, button, langBtn)); - wrapper.appendChild(button); + button.addEventListener('click', () => onEnhanceClick(textarea, button, langBtn, actionWrap)); + actionWrap.appendChild(button); + syncActionGroupState(button, langBtn, actionWrap); + bindActionDimensionSync(textarea, wrapper, actionWrap); textarea.dataset.promptEnhancerMounted = '1'; } @@ -413,3 +647,4 @@ init(); } })(); + diff --git a/app/static/common/js/public-header.js b/app/static/common/js/public-header.js index 35459bd..0d4d90a 100644 --- a/app/static/common/js/public-header.js +++ b/app/static/common/js/public-header.js @@ -2,16 +2,27 @@ async function loadPublicHeader() { const container = document.getElementById('app-header'); if (!container) return; try { - const res = await fetch('/static/common/html/public-header.html?v=0.3.0'); + const res = await fetch('/static/common/html/public-header.html?v=0.3.3'); if (!res.ok) return; container.innerHTML = await res.text(); - const logoutBtn = container.querySelector('#public-logout-btn'); - if (logoutBtn) { - logoutBtn.classList.add('hidden'); + const desktopLogoutBtn = container.querySelector('#public-logout-btn'); + const mobileLogoutBtn = container.querySelector('#public-mobile-logout-btn'); + if (desktopLogoutBtn) { + desktopLogoutBtn.classList.add('hidden'); + } + if (mobileLogoutBtn) { + mobileLogoutBtn.classList.add('hidden'); + } + if (desktopLogoutBtn || mobileLogoutBtn) { try { const verify = await fetch('/v1/public/verify', { method: 'GET' }); if (verify.status === 401) { - logoutBtn.classList.remove('hidden'); + if (desktopLogoutBtn) { + desktopLogoutBtn.classList.remove('hidden'); + } + if (mobileLogoutBtn) { + mobileLogoutBtn.classList.remove('hidden'); + } } } catch (e) { // Ignore verification errors and keep it hidden @@ -25,6 +36,65 @@ async function loadPublicHeader() { link.classList.add('active'); } }); + + const nav = container.querySelector('nav'); + const menuToggle = container.querySelector('#public-menu-toggle'); + const mobileMenu = container.querySelector('#public-mobile-menu'); + if (menuToggle && mobileMenu) { + const setMenuOpen = (open) => { + const isOpen = Boolean(open); + menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); + mobileMenu.hidden = !isOpen; + mobileMenu.classList.toggle('open', isOpen); + if (nav) { + nav.classList.toggle('mobile-menu-open', isOpen); + } + document.body.classList.toggle('mobile-menu-open', isOpen); + }; + + setMenuOpen(false); + + menuToggle.addEventListener('click', () => { + const expanded = menuToggle.getAttribute('aria-expanded') === 'true'; + setMenuOpen(!expanded); + }); + + mobileMenu.addEventListener('click', (event) => { + const target = event.target; + if (!(target instanceof Element)) return; + if (target.closest('a') || target.closest('button')) { + setMenuOpen(false); + } + }); + + document.addEventListener('click', (event) => { + const target = event.target; + if (!(target instanceof Node)) return; + if (!container.contains(target)) { + setMenuOpen(false); + } + }); + + document.addEventListener('keydown', (event) => { + if (event.key === 'Escape') { + setMenuOpen(false); + } + }); + + window.addEventListener('resize', () => { + if (window.innerWidth > 900) { + setMenuOpen(false); + } + }); + } + + if (window.themeController && typeof window.themeController.refreshButtons === 'function') { + window.themeController.refreshButtons(); + } + + if (typeof window.setupPwaInstallButtons === 'function') { + window.setupPwaInstallButtons(container); + } } catch (e) { // Fail silently to avoid breaking page load } diff --git a/app/static/common/js/pwa.js b/app/static/common/js/pwa.js new file mode 100644 index 0000000..9d9fef1 --- /dev/null +++ b/app/static/common/js/pwa.js @@ -0,0 +1,109 @@ +(() => { + if (!("serviceWorker" in navigator)) return; + + const isLocalhost = + window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1"; + if (!window.isSecureContext && !isLocalhost) return; + + let deferredPrompt = null; + let installMode = "none"; + + function isStandalone() { + return window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true; + } + + function isIosDevice() { + const ua = window.navigator.userAgent || ""; + return /iphone|ipad|ipod/i.test(ua); + } + + function toast(message, type) { + if (typeof window.showToast === "function") { + window.showToast(message, type); + } + } + + function getInstallButtons(scope) { + const root = scope instanceof Element || scope instanceof Document ? scope : document; + return Array.from(root.querySelectorAll("[data-pwa-install-btn]")); + } + + function refreshInstallButtons(scope) { + const buttons = getInstallButtons(scope); + const canShow = !isStandalone() && (installMode === "prompt" || installMode === "ios"); + buttons.forEach((btn) => { + btn.classList.toggle("hidden", !canShow); + btn.disabled = !canShow; + }); + } + + async function runInstallPrompt() { + if (isStandalone()) return; + if (installMode === "ios") { + toast("iOS 请点击浏览器分享按钮,再选择“添加到主屏幕”", "success"); + return; + } + if (!deferredPrompt) { + toast("当前环境暂不支持安装", "warning"); + return; + } + try { + deferredPrompt.prompt(); + await deferredPrompt.userChoice; + } finally { + deferredPrompt = null; + installMode = isIosDevice() ? "ios" : "none"; + refreshInstallButtons(); + } + } + + function bindInstallButtonClick(scope) { + const buttons = getInstallButtons(scope); + buttons.forEach((btn) => { + if (btn.dataset.pwaBound === "1") return; + btn.dataset.pwaBound = "1"; + btn.addEventListener("click", () => { + runInstallPrompt(); + }); + }); + } + + window.setupPwaInstallButtons = (scope) => { + bindInstallButtonClick(scope); + refreshInstallButtons(scope); + }; + + window.addEventListener("beforeinstallprompt", (event) => { + event.preventDefault(); + deferredPrompt = event; + installMode = "prompt"; + refreshInstallButtons(); + }); + + window.addEventListener("appinstalled", () => { + deferredPrompt = null; + installMode = "none"; + refreshInstallButtons(); + }); + + window.addEventListener("load", () => { + navigator.serviceWorker.register("/sw.js?v=0.1.2").catch(() => {}); + if (isIosDevice() && !isStandalone()) { + installMode = "ios"; + } + window.setupPwaInstallButtons(document); + }); + + const observer = new MutationObserver(() => { + window.setupPwaInstallButtons(document); + }); + if (document.body) { + observer.observe(document.body, { childList: true, subtree: true }); + } else { + window.addEventListener("DOMContentLoaded", () => { + observer.observe(document.body, { childList: true, subtree: true }); + window.setupPwaInstallButtons(document); + }); + } +})(); diff --git a/app/static/common/js/theme.js b/app/static/common/js/theme.js new file mode 100644 index 0000000..89f6b45 --- /dev/null +++ b/app/static/common/js/theme.js @@ -0,0 +1,119 @@ +(() => { + const THEME_KEY = 'grok2api_theme_preference'; + const THEME_LIGHT = 'light'; + const THEME_DARK = 'dark'; + const THEME_AUTO = 'auto'; + + const metaThemeColor = document.querySelector('meta[name="theme-color"]'); + + function safeGetPreference() { + try { + const stored = localStorage.getItem(THEME_KEY); + if (stored === THEME_LIGHT || stored === THEME_DARK || stored === THEME_AUTO) { + return stored; + } + } catch (e) { + // 忽略 localStorage 访问异常 + } + return THEME_AUTO; + } + + function safeSetPreference(preference) { + try { + localStorage.setItem(THEME_KEY, preference); + } catch (e) { + // 忽略 localStorage 写入异常 + } + } + + function resolvePreference(preference) { + if (preference === THEME_LIGHT || preference === THEME_DARK) { + return preference; + } + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + return THEME_DARK; + } + return THEME_LIGHT; + } + + function syncThemeMeta(resolvedTheme) { + if (!metaThemeColor) return; + metaThemeColor.setAttribute('content', resolvedTheme === THEME_DARK ? '#0b0f14' : '#fafafa'); + } + + function syncThemeButtons(preference, resolvedTheme) { + const buttons = document.querySelectorAll('[data-theme-toggle-btn]'); + buttons.forEach((btn) => { + const nextLabel = resolvedTheme === THEME_DARK ? '日间模式' : '深夜模式'; + const nextTitle = resolvedTheme === THEME_DARK ? '切换到日间模式' : '切换到深夜模式'; + btn.textContent = nextLabel; + btn.setAttribute('title', nextTitle); + btn.classList.toggle('theme-active', resolvedTheme === THEME_DARK); + btn.setAttribute('aria-pressed', resolvedTheme === THEME_DARK ? 'true' : 'false'); + btn.setAttribute('data-theme-preference', preference); + btn.setAttribute('data-theme-resolved', resolvedTheme); + }); + } + + function applyTheme(preference, persist = false) { + const normalized = (preference === THEME_LIGHT || preference === THEME_DARK || preference === THEME_AUTO) + ? preference + : THEME_AUTO; + const resolved = resolvePreference(normalized); + const root = document.documentElement; + root.setAttribute('data-theme-preference', normalized); + root.setAttribute('data-theme', resolved); + syncThemeMeta(resolved); + syncThemeButtons(normalized, resolved); + if (persist) { + safeSetPreference(normalized); + } + } + + function toggleTheme() { + const current = document.documentElement.getAttribute('data-theme') === THEME_DARK ? THEME_DARK : THEME_LIGHT; + const next = current === THEME_DARK ? THEME_LIGHT : THEME_DARK; + applyTheme(next, true); + } + + function handleThemeButtonClick(event) { + const target = event.target; + if (!(target instanceof Element)) return; + const button = target.closest('[data-theme-toggle-btn]'); + if (!button) return; + event.preventDefault(); + toggleTheme(); + } + + function init() { + applyTheme(safeGetPreference(), false); + document.addEventListener('click', handleThemeButtonClick); + + if (window.matchMedia) { + const media = window.matchMedia('(prefers-color-scheme: dark)'); + const onSystemThemeChange = () => { + if (safeGetPreference() === THEME_AUTO) { + applyTheme(THEME_AUTO, false); + } + }; + if (typeof media.addEventListener === 'function') { + media.addEventListener('change', onSystemThemeChange); + } else if (typeof media.addListener === 'function') { + media.addListener(onSystemThemeChange); + } + } + } + + window.themeController = { + applyTheme, + refreshButtons() { + const preference = document.documentElement.getAttribute('data-theme-preference') || safeGetPreference(); + const resolved = document.documentElement.getAttribute('data-theme') || resolvePreference(preference); + syncThemeButtons(preference, resolved); + syncThemeMeta(resolved); + }, + getPreference: safeGetPreference, + }; + + init(); +})(); diff --git a/app/static/public/css/imagine_workbench.css b/app/static/public/css/imagine_workbench.css index 1830c8a..40c755e 100644 --- a/app/static/public/css/imagine_workbench.css +++ b/app/static/public/css/imagine_workbench.css @@ -225,28 +225,38 @@ body { display: block; box-sizing: border-box; min-height: 150px; - padding-left: 112px; - padding-right: 116px; - padding-bottom: 44px; + padding-left: 12px; + padding-right: calc(var(--pe-action-width, 104px) + 12px); + padding-bottom: 82px; +} + +.field .prompt-enhance-wrap > .inline-submit-btn, +.field .prompt-enhance-wrap > .prompt-enhance-actions { + position: absolute; + right: 8px; + z-index: 3; + height: 30px; } .field .prompt-enhance-wrap > .inline-submit-btn { - position: absolute; - left: 8px; - bottom: 8px; - z-index: 3; - height: 30px; - min-width: 88px; + width: var(--pe-action-width, 104px); + min-width: var(--pe-action-width, 104px); padding: 0 10px; } -.field .prompt-enhance-wrap > .prompt-enhance-btn { - right: 8px; - bottom: 8px; +.field .prompt-enhance-wrap > .prompt-enhance-actions { + width: var(--pe-action-width, 104px); + min-width: var(--pe-action-width, 104px); + padding: 0; } -.field .prompt-enhance-wrap > .prompt-lang-toggle-btn { - right: 66px; +.field .prompt-enhance-wrap > .prompt-enhance-actions { + right: 8px; + bottom: 44px; +} + +.field .prompt-enhance-wrap > .inline-submit-btn { + right: 8px; bottom: 8px; } @@ -316,6 +326,35 @@ body { } } +@media (max-width: 768px) { + .field .prompt-enhance-wrap > textarea.form-textarea { + padding-right: calc(var(--pe-action-width, 92px) + 12px); + padding-bottom: 76px; + } + + .field .prompt-enhance-wrap > .inline-submit-btn, + .field .prompt-enhance-wrap > .prompt-enhance-actions { + height: var(--pe-action-height, 28px); + } + + .field .prompt-enhance-wrap > .inline-submit-btn { + width: var(--pe-action-width, 92px); + min-width: var(--pe-action-width, 92px); + padding: 0 10px; + } + + .field .prompt-enhance-wrap > .prompt-enhance-actions { + width: var(--pe-action-width, 92px); + min-width: var(--pe-action-width, 92px); + bottom: 40px; + } + + .field .prompt-enhance-wrap > .inline-submit-btn { + bottom: 8px; + } + +} + .empty-state { border: 1px dashed var(--workbench-border); border-radius: 10px; @@ -379,11 +418,28 @@ body { .history-prompt { margin-top: 4px; font-size: 12px; - color: #111827; + color: var(--workbench-title); line-height: 1.5; word-break: break-word; } +html[data-theme='dark'] .history-item { + background: #111821; + border-color: #324154; +} + +html[data-theme='dark'] .history-line { + color: #b7c4d4; +} + +html[data-theme='dark'] .history-line strong { + color: #eef4ff; +} + +html[data-theme='dark'] .history-prompt { + color: #e8effa; +} + .history-actions { margin-top: 8px; display: flex; diff --git a/app/static/public/css/video.css b/app/static/public/css/video.css index 7744a98..96153f2 100644 --- a/app/static/public/css/video.css +++ b/app/static/public/css/video.css @@ -12,6 +12,40 @@ body { background-color: var(--video-bg); background-image: none; overflow-x: hidden; + max-width: 100vw; + width: 100%; +} + +/* 移动端强制约束 */ +@media (max-width: 768px) { + .video-main-grid { + display: flex; + flex-direction: column; + width: 100%; + min-width: 0; + } + + .video-main-left, + .video-main-right { + width: 100%; + min-width: 0; + max-width: 100%; + } + + .video-card { + width: 100%; + max-width: 100%; + box-sizing: border-box; + min-width: 0; + } + + /* 修复编辑面板溢出 */ + #editPanel { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + overflow-x: hidden; + } } .video-header-row { @@ -69,6 +103,15 @@ body { display: flex; gap: 8px; flex-wrap: wrap; + /* Allow wrapping on small screens */ + width: 100%; +} + +.video-actions button { + flex: 1 1 auto; + /* Allow buttons to shrink/grow */ + min-width: 0; + white-space: nowrap; } .video-top-grid { @@ -93,7 +136,7 @@ body { } /* 生成设置卡片始终随文档流滚动,禁止任何置顶行为 */ -.video-main-left > .video-card:first-child { +.video-main-left>.video-card:first-child { position: static !important; top: auto !important; z-index: auto !important; @@ -468,6 +511,7 @@ body { 0% { transform: translateX(-120%); } + 100% { transform: translateX(220%); } @@ -1001,9 +1045,33 @@ html[data-theme='dark'] .cache-video-item.is-active .cache-video-thumb-wrap { } .settings-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - grid-template-rows: auto; + display: grid; + grid-template-columns: 1fr !important; + /* Force single column */ + width: 100% !important; + max-width: 100% !important; gap: 8px; + min-width: 0 !important; + } + + .settings-block { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + box-sizing: border-box; + } + + .settings-actions { + width: 100% !important; + max-width: 100% !important; + flex-wrap: wrap; + min-width: 0 !important; + } + + .settings-actions button { + flex: 1 1 auto; + min-width: 0; + max-width: 100%; } .prompt-block, @@ -1099,8 +1167,9 @@ html[data-theme='dark'] .cache-video-item.is-active .cache-video-thumb-wrap { width: min(100%, 420px); margin-left: auto; margin-right: auto; - height: 320px; - max-height: 320px; + height: auto; + aspect-ratio: 3/2; + /* Removed fixed height 320px to prevent deformation on mobile */ display: flex; align-items: center; justify-content: center; @@ -1114,7 +1183,7 @@ html[data-theme='dark'] .cache-video-item.is-active .cache-video-thumb-wrap { inset: 0; width: 100%; height: 100%; - max-height: 320px; + /* max-height: 320px; Removed to follow container */ object-fit: contain; } @@ -1159,7 +1228,13 @@ html[data-theme='dark'] .cache-video-item.is-active .cache-video-thumb-wrap { .settings-actions .geist-button, .settings-actions .geist-button-outline { - flex: 1; + flex: 1 1 0%; + /* Force equal width, side-by-side */ + min-width: 0; + padding-left: 4px; + padding-right: 4px; justify-content: center; + width: auto !important; + /* Allow buttons to shrink */ } -} +} \ No newline at end of file diff --git a/app/static/public/css/voice.css b/app/static/public/css/voice.css index da511c1..225184d 100644 --- a/app/static/public/css/voice.css +++ b/app/static/public/css/voice.css @@ -249,3 +249,11 @@ body { border-radius: 999px; background: #111; } + +@media (max-width: 900px) { + .voice-actions, + #clearLogBtn, + #copyLogBtn { + display: none !important; + } +} diff --git a/app/static/public/js/imagine.js b/app/static/public/js/imagine.js index c27d928..9ef800e 100644 --- a/app/static/public/js/imagine.js +++ b/app/static/public/js/imagine.js @@ -57,6 +57,7 @@ let editDurationEstimateMs = 14000; let wsPausedByEdit = false; let lightboxImageFullscreen = false; + let lightboxEditAbortController = null; let finalMinBytesDefault = 100000; const lightboxHistoryByItem = new WeakMap(); if (lightboxEditSend) { @@ -402,6 +403,26 @@ }, 900); } + function setLightboxEditButtonState(running) { + if (!lightboxEditSend) return; + lightboxEditSend.dataset.running = running ? '1' : '0'; + if (running) { + lightboxEditSend.textContent = '中止'; + lightboxEditSend.disabled = false; + return; + } + lightboxEditSend.textContent = '发送编辑'; + const currentItem = getItemByImageIndex(currentImageIndex); + const currentParent = currentItem ? String(currentItem.dataset.parentPostId || '').trim() : ''; + lightboxEditSend.disabled = !currentParent; + } + + function cancelLightboxEdit() { + if (lightboxEditAbortController) { + lightboxEditAbortController.abort(); + } + } + function estimateBase64Bytes(raw) { if (!raw) return null; if (raw.startsWith('http://') || raw.startsWith('https://')) { @@ -496,7 +517,7 @@ return await res.json(); } - async function requestImagineEditStream(authHeader, prompt, parentPostId, sourceImageUrl, onProgress) { + async function requestImagineEditStream(authHeader, prompt, parentPostId, sourceImageUrl, onProgress, signal) { const res = await fetch('/v1/public/imagine/edit', { method: 'POST', headers: { @@ -509,6 +530,7 @@ source_image_url: sourceImageUrl, stream: true, }), + signal, }); if (!res.ok) { @@ -1841,7 +1863,10 @@ renderLightboxHistory(item); if (lightboxEditSend) { const parentPostId = item ? String(item.dataset.parentPostId || '').trim() : ''; - lightboxEditSend.disabled = !parentPostId; + if (String(lightboxEditSend.dataset.running || '0') !== '1') { + lightboxEditSend.disabled = !parentPostId; + lightboxEditSend.textContent = '发送编辑'; + } lightboxEditSend.title = parentPostId ? '使用 parentPostId 发起编辑' : '当前图片缺少 parentPostId,无法编辑'; if (lightboxEditInput && !lightboxEditInput.value.trim()) { const seedPrompt = item ? String(item.dataset.prompt || '').trim() : ''; @@ -1870,6 +1895,10 @@ } async function startEditFromLightbox() { + if (lightboxEditSend && String(lightboxEditSend.dataset.running || '0') === '1') { + cancelLightboxEdit(); + return; + } const item = getItemByImageIndex(currentImageIndex); if (!item) { toast('未找到当前图片', 'error'); @@ -1901,9 +1930,11 @@ parentPostId, String(item.dataset.sourceImageUrl || '').trim() ); + lightboxEditAbortController = new AbortController(); if (lightboxEditSend) { - lightboxEditSend.disabled = true; - lightboxEditSend.textContent = '编辑中...'; + lightboxEditSend.dataset.running = '1'; + lightboxEditSend.textContent = '中止'; + lightboxEditSend.disabled = false; } if (lightboxEditInput) { lightboxEditInput.disabled = true; @@ -1925,7 +1956,8 @@ } else if (text) { setEditProgress(editProgressValue, text); } - } + }, + lightboxEditAbortController ? lightboxEditAbortController.signal : undefined ); const list = (data && Array.isArray(data.data)) ? data.data : []; const first = list.length ? list[0] : null; @@ -1996,15 +2028,19 @@ lightboxEditInput.value = ''; } } catch (e) { + if (e && e.name === 'AbortError') { + finishEditProgress(false, '已中止'); + toast('已中止编辑', 'warning'); + return; + } const msg = String(e && e.message ? e.message : e); finishEditProgress(false, '编辑失败'); toast(`编辑失败:${msg}`, 'error'); } finally { + lightboxEditAbortController = null; if (lightboxEditSend) { - lightboxEditSend.textContent = '发送编辑'; - const currentItem = getItemByImageIndex(currentImageIndex); - const currentParent = currentItem ? String(currentItem.dataset.parentPostId || '').trim() : ''; - lightboxEditSend.disabled = !currentParent; + lightboxEditSend.dataset.running = '0'; + setLightboxEditButtonState(false); } if (lightboxEditInput) { lightboxEditInput.disabled = false; @@ -2015,6 +2051,7 @@ if (lightbox && closeLightbox) { closeLightbox.addEventListener('click', (e) => { e.stopPropagation(); + cancelLightboxEdit(); setLightboxImageFullscreen(false); lightbox.classList.remove('active'); setLightboxKeyboardShift(0); @@ -2033,6 +2070,7 @@ }); lightbox.addEventListener('click', () => { + cancelLightboxEdit(); setLightboxImageFullscreen(false); lightbox.classList.remove('active'); setLightboxKeyboardShift(0); @@ -2061,6 +2099,10 @@ if (lightboxEditSend) { lightboxEditSend.addEventListener('click', async (e) => { e.stopPropagation(); + if (String(lightboxEditSend.dataset.running || '0') === '1') { + cancelLightboxEdit(); + return; + } await startEditFromLightbox(); }); } @@ -2113,6 +2155,7 @@ setLightboxImageFullscreen(false); return; } + cancelLightboxEdit(); lightbox.classList.remove('active'); setLightboxKeyboardShift(0); currentImageIndex = -1; diff --git a/app/static/public/js/imagine_workbench.js b/app/static/public/js/imagine_workbench.js index b20ea81..91a0c8f 100644 --- a/app/static/public/js/imagine_workbench.js +++ b/app/static/public/js/imagine_workbench.js @@ -1,4 +1,4 @@ -(() => { +(() => { const seedImageInput = document.getElementById('seedImageInput'); const selectSeedBtn = document.getElementById('selectSeedBtn'); const seedFileName = document.getElementById('seedFileName'); @@ -32,6 +32,7 @@ history: [], editRound: 0, }; + let workbenchEditAbortController = null; let editProgressTimer = null; let editProgressHideTimer = null; let editProgressValue = 0; @@ -406,7 +407,13 @@ function setEditing(loading) { state.editing = Boolean(loading); - if (submitEditBtn) submitEditBtn.disabled = state.editing; + if (submitEditBtn) { + submitEditBtn.disabled = false; + submitEditBtn.dataset.running = state.editing ? '1' : '0'; + submitEditBtn.textContent = state.editing ? '中止' : '执行编辑'; + submitEditBtn.classList.toggle('is-editing', state.editing); + submitEditBtn.removeAttribute('aria-disabled'); + } if (selectSeedBtn) selectSeedBtn.disabled = state.editing; if (resetCycleBtn) resetCycleBtn.disabled = state.editing; if (clearHistoryBtn) clearHistoryBtn.disabled = state.editing; @@ -414,12 +421,19 @@ if (state.editing) { setStatus('running', '编辑中...'); - if (submitEditBtn) submitEditBtn.textContent = '编辑中...'; - } else { - if (submitEditBtn) submitEditBtn.textContent = '执行编辑'; } } + function forceSubmitButtonAbortClickable() { + if (!submitEditBtn) return; + if (!state.editing) return; + submitEditBtn.disabled = false; + submitEditBtn.dataset.running = '1'; + submitEditBtn.textContent = '中止'; + submitEditBtn.classList.add('is-editing'); + submitEditBtn.removeAttribute('aria-disabled'); + } + function setCurrentFromEntry(entry) { if (!entry) return; state.currentParentPostId = String(entry.parentPostId || '').trim(); @@ -466,11 +480,11 @@ const line1 = document.createElement('div'); line1.className = 'history-line'; - line1.innerHTML = `#${entry.round} · ${formatTime(entry.createdAt)} · ${entry.elapsedMs}ms`; + line1.innerHTML = `#${entry.round} 路 ${formatTime(entry.createdAt)} 路 ${entry.elapsedMs}ms`; const line2 = document.createElement('div'); line2.className = 'history-line'; - line2.innerHTML = `mode=${entry.mode} · parentPostId=${shortId(entry.parentPostId)}`; + line2.innerHTML = `mode=${entry.mode} 路 parentPostId=${shortId(entry.parentPostId)}`; const prompt = document.createElement('div'); prompt.className = 'history-prompt'; @@ -558,7 +572,7 @@ return text; } - async function requestWorkbenchEditStream(authHeader, body, onProgress) { + async function requestWorkbenchEditStream(authHeader, body, onProgress, signal) { const payload = { ...body, stream: true, @@ -570,6 +584,7 @@ 'Content-Type': 'application/json', }, body: JSON.stringify(payload), + signal, }); if (!res.ok) { @@ -663,7 +678,12 @@ } async function runEdit() { - if (state.editing) return; + if (state.editing) { + if (workbenchEditAbortController) { + workbenchEditAbortController.abort(); + } + return; + } const prompt = String(editPromptInput ? editPromptInput.value : '').trim(); if (!prompt) { @@ -701,10 +721,13 @@ } setEditing(true); + forceSubmitButtonAbortClickable(); startEditProgress(); + workbenchEditAbortController = new AbortController(); try { const payload = await requestWorkbenchEditStream(authHeader, body, (evt) => { + forceSubmitButtonAbortClickable(); const next = Number(evt && evt.progress ? evt.progress : 0); const message = String((evt && evt.message) || '').trim(); if (Number.isFinite(next) && next > 0) { @@ -715,7 +738,7 @@ setEditProgress(editProgressValue, message); setStatus('running', message); } - }); + }, workbenchEditAbortController ? workbenchEditAbortController.signal : undefined); const imageUrl = String(payload?.data?.[0]?.url || '').trim(); if (!imageUrl) { throw new Error('返回结果缺少图片 URL'); @@ -777,10 +800,17 @@ setStatus('done', `编辑完成 · round #${entry.round}`); toast('编辑成功,已更新当前画面', 'success'); } catch (e) { - finishEditProgress(false, '编辑失败'); - setStatus('error', '编辑失败'); - toast(String(e.message || e), 'error'); + if (e && e.name === 'AbortError') { + finishEditProgress(false, '已中止'); + setStatus('error', '已中止'); + toast('已中止编辑', 'warning'); + } else { + finishEditProgress(false, '编辑失败'); + setStatus('error', '编辑失败'); + toast(String(e.message || e), 'error'); + } } finally { + workbenchEditAbortController = null; setEditing(false); } } @@ -796,7 +826,7 @@ if (!file) return; try { - await applySeedImageFile(file, '本地图片'); + await applySeedImageFile(file, '本地图像'); } catch (e) { toast(String(e.message || e), 'error'); } finally { @@ -976,3 +1006,6 @@ init(); })(); + + + diff --git a/app/static/public/js/nsfw.js b/app/static/public/js/nsfw.js index cfb537b..8571f03 100644 --- a/app/static/public/js/nsfw.js +++ b/app/static/public/js/nsfw.js @@ -1,4 +1,4 @@ -(() => { +(() => { const imagePromptInput = document.getElementById('imagePromptInput'); const videoPromptInput = document.getElementById('videoPromptInput'); const ratioSelect = document.getElementById('ratioSelect'); @@ -64,6 +64,7 @@ editDurationEstimateMs: 14000, imageFullscreen: false, }; + let lightboxEditAbortController = null; if (lightboxEditSend) { lightboxEditSend.disabled = true; } @@ -143,15 +144,28 @@ } } + function setToggleButtonState(button, running, runningText) { + if (!button) return; + if (!button.dataset.defaultText) { + button.dataset.defaultText = String(button.textContent || '').trim() || ''; + } + button.dataset.running = running ? '1' : '0'; + button.textContent = running ? runningText : (button.dataset.defaultText || button.textContent); + } + function updateImageButtons() { - if (generateBatchBtn) generateBatchBtn.disabled = state.imageRunning; + const running = state.imageRunning; + setToggleButtonState(generateBatchBtn, running, '中止'); + if (generateBatchBtn) generateBatchBtn.disabled = false; if (nextBatchBtn) nextBatchBtn.disabled = state.imageRunning; if (stopBatchBtn) stopBatchBtn.disabled = !state.imageRunning; } function updateVideoButtons() { const hasSelected = Boolean(getSelectedCandidate()); - if (startVideoBtn) startVideoBtn.disabled = state.videoRunning || !hasSelected; + const running = state.videoRunning; + setToggleButtonState(startVideoBtn, running, '中止'); + if (startVideoBtn) startVideoBtn.disabled = running ? false : !hasSelected; if (stopVideoBtn) stopVideoBtn.disabled = !state.videoRunning; } @@ -317,7 +331,7 @@ } } - async function requestImagineEditStream(authHeader, prompt, parentPostId, sourceImageUrl, onProgress) { + async function requestImagineEditStream(authHeader, prompt, parentPostId, sourceImageUrl, onProgress, signal) { const res = await fetch('/v1/public/imagine/edit', { method: 'POST', headers: { @@ -330,6 +344,7 @@ source_image_url: sourceImageUrl, stream: true, }), + signal, }); if (!res.ok) { @@ -577,6 +592,26 @@ }, 900); } + function setLightboxEditButtonState(running) { + if (!lightboxEditSend) return; + lightboxEditSend.dataset.running = running ? '1' : '0'; + if (running) { + lightboxEditSend.textContent = '中止'; + lightboxEditSend.disabled = false; + return; + } + lightboxEditSend.textContent = '发送编辑'; + const current = getCurrentLightboxCandidate(); + const currentParent = current ? String(current.parentPostId || '').trim() : ''; + lightboxEditSend.disabled = !currentParent; + } + + function cancelLightboxEdit() { + if (lightboxEditAbortController) { + lightboxEditAbortController.abort(); + } + } + function renderLightboxHistory(candidate) { if (!lightboxHistoryCount || !lightboxHistoryEmpty || !lightboxHistoryList) return; lightboxHistoryList.innerHTML = ''; @@ -707,7 +742,9 @@ if (lightboxNext) lightboxNext.disabled = (index === state.candidates.length - 1); if (lightboxEditSend) { const parentPostId = String(candidate.parentPostId || '').trim(); - lightboxEditSend.disabled = !parentPostId; + if (String(lightboxEditSend.dataset.running || '0') !== '1') { + setLightboxEditButtonState(false); + } lightboxEditSend.title = parentPostId ? '使用 parentPostId 发起编辑' : '当前图片缺少 parentPostId,无法编辑'; if (lightboxEditInput && !lightboxEditInput.value.trim()) { const seedPrompt = String(candidate.prompt || '').trim(); @@ -732,11 +769,13 @@ function closeLightboxView() { if (!lightbox) return; + cancelLightboxEdit(); setLightboxImageFullscreen(false); lightbox.classList.remove('active'); setLightboxKeyboardShift(0); state.lightboxIndex = -1; if (lightboxEditSend) { + lightboxEditSend.dataset.running = '0'; lightboxEditSend.textContent = '发送编辑'; lightboxEditSend.disabled = true; } @@ -789,10 +828,8 @@ [candidate.sourceImageUrl, candidate.imageUrl], parentPostId ); - if (lightboxEditSend) { - lightboxEditSend.disabled = true; - lightboxEditSend.textContent = '编辑中...'; - } + lightboxEditAbortController = new AbortController(); + setLightboxEditButtonState(true); if (lightboxEditInput) { lightboxEditInput.disabled = true; } @@ -814,7 +851,8 @@ } else if (text) { setEditProgress(state.editProgressValue, text); } - } + }, + lightboxEditAbortController ? lightboxEditAbortController.signal : undefined ); const list = (data && Array.isArray(data.data)) ? data.data : []; const first = list.length ? list[0] : null; @@ -878,16 +916,17 @@ lightboxEditInput.value = ''; } } catch (e) { + if (e && e.name === 'AbortError') { + finishEditProgress(false, '已中止'); + toast('已中止编辑', 'warning'); + return; + } const msg = String(e && e.message ? e.message : e); finishEditProgress(false, '编辑失败'); toast(`编辑失败:${msg}`, 'error'); } finally { - if (lightboxEditSend) { - lightboxEditSend.textContent = '发送编辑'; - const current = getCurrentLightboxCandidate(); - const currentParent = current ? String(current.parentPostId || '').trim() : ''; - lightboxEditSend.disabled = !currentParent; - } + lightboxEditAbortController = null; + setLightboxEditButtonState(false); if (lightboxEditInput) { lightboxEditInput.disabled = false; } @@ -1628,6 +1667,10 @@ if (generateBatchBtn) { generateBatchBtn.addEventListener('click', () => { + if (String(generateBatchBtn.dataset.running || '0') === '1') { + stopImageBatch(false); + return; + } startImageBatch(); }); } @@ -1649,6 +1692,10 @@ if (startVideoBtn) { startVideoBtn.addEventListener('click', () => { + if (String(startVideoBtn.dataset.running || '0') === '1') { + stopVideos(false); + return; + } startVideos(); }); } @@ -1701,6 +1748,10 @@ if (lightboxEditSend) { lightboxEditSend.addEventListener('click', async (e) => { e.stopPropagation(); + if (String(lightboxEditSend.dataset.running || '0') === '1') { + cancelLightboxEdit(); + return; + } await startEditFromLightbox(); }); } diff --git a/app/static/public/js/video.js b/app/static/public/js/video.js index 19e3676..21a92dc 100644 --- a/app/static/public/js/video.js +++ b/app/static/public/js/video.js @@ -142,6 +142,7 @@ } function lockWorkspacePreviewSize(force = false) { + if (window.innerWidth < 640) return; // Disable on mobile to allow fluid width if (!editPreviewWrap || !editVideo) return; if (!shouldLockWorkspacePreviewSize()) { editPreviewWrap.style.removeProperty('width'); diff --git a/app/static/public/manifest.webmanifest b/app/static/public/manifest.webmanifest new file mode 100644 index 0000000..10811cf --- /dev/null +++ b/app/static/public/manifest.webmanifest @@ -0,0 +1,24 @@ +{ + "name": "Grok2API", + "short_name": "Grok2API", + "description": "Grok2API 功能工作台", + "start_url": "/login", + "scope": "/", + "display": "standalone", + "background_color": "#fafafa", + "theme_color": "#fafafa", + "icons": [ + { + "src": "/static/public/pwa/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, + { + "src": "/static/public/pwa/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/app/static/public/pages/imagine.html b/app/static/public/pages/imagine.html index 7aa6165..4780d18 100644 --- a/app/static/public/pages/imagine.html +++ b/app/static/public/pages/imagine.html @@ -291,7 +291,7 @@ - + diff --git a/app/static/public/pages/imagine_workbench.html b/app/static/public/pages/imagine_workbench.html index 21de408..00c54ae 100644 --- a/app/static/public/pages/imagine_workbench.html +++ b/app/static/public/pages/imagine_workbench.html @@ -19,7 +19,7 @@ - + @@ -129,7 +129,7 @@ - + diff --git a/app/static/public/pages/nsfw.html b/app/static/public/pages/nsfw.html index e32ea89..d080035 100644 --- a/app/static/public/pages/nsfw.html +++ b/app/static/public/pages/nsfw.html @@ -201,7 +201,7 @@ - + diff --git a/app/static/public/pages/video.html b/app/static/public/pages/video.html index a82448b..b41de71 100644 --- a/app/static/public/pages/video.html +++ b/app/static/public/pages/video.html @@ -296,7 +296,7 @@ - + diff --git a/app/static/public/pwa/apple-touch-icon.png b/app/static/public/pwa/apple-touch-icon.png new file mode 100644 index 0000000..c50fc9c Binary files /dev/null and b/app/static/public/pwa/apple-touch-icon.png differ diff --git a/app/static/public/pwa/icon-192.png b/app/static/public/pwa/icon-192.png new file mode 100644 index 0000000..d732331 Binary files /dev/null and b/app/static/public/pwa/icon-192.png differ diff --git a/app/static/public/pwa/icon-512.png b/app/static/public/pwa/icon-512.png new file mode 100644 index 0000000..e645736 Binary files /dev/null and b/app/static/public/pwa/icon-512.png differ diff --git a/app/static/public/sw.js b/app/static/public/sw.js new file mode 100644 index 0000000..6e8bcd9 --- /dev/null +++ b/app/static/public/sw.js @@ -0,0 +1,56 @@ +const CACHE_PREFIX = "grok2api-pwa-"; +const CACHE_NAME = `${CACHE_PREFIX}v2`; + +self.addEventListener("install", (event) => { + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + (async () => { + const keys = await caches.keys(); + await Promise.all( + keys + .filter((key) => key.startsWith(CACHE_PREFIX) && key !== CACHE_NAME) + .map((key) => caches.delete(key)) + ); + await self.clients.claim(); + })() + ); +}); + +self.addEventListener("fetch", (event) => { + const request = event.request; + if (request.method !== "GET") return; + + const url = new URL(request.url); + if (url.origin !== self.location.origin) return; + + const cacheablePathPrefixes = [ + "/static/", + "/login", + "/imagine", + "/video", + "/voice", + "/nsfw", + "/imagine-workbench", + "/manifest.webmanifest" + ]; + const isCacheable = cacheablePathPrefixes.some((prefix) => url.pathname.startsWith(prefix)); + if (!isCacheable) return; + + event.respondWith( + (async () => { + try { + const response = await fetch(request); + const cache = await caches.open(CACHE_NAME); + cache.put(request, response.clone()); + return response; + } catch (error) { + const cached = await caches.match(request); + if (cached) return cached; + throw error; + } + })() + ); +}); diff --git a/app/static/vendor/ffmpeg/814.ffmpeg.js b/app/static/vendor/ffmpeg/814.ffmpeg.js new file mode 100644 index 0000000..a82c120 --- /dev/null +++ b/app/static/vendor/ffmpeg/814.ffmpeg.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{"use strict";const e="https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.js";var t;!function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const r=new Error("unknown message type"),a=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),s=(new Error("called FFmpeg.terminate()"),new Error("failed to import ffmpeg-core.js"));let o;return self.onmessage=async({data:{id:n,type:E,data:c}})=>{const i=[];let p;try{if(E!==t.LOAD&&!o)throw a;switch(E){case t.LOAD:p=await(async({coreURL:r,wasmURL:a,workerURL:n})=>{const E=!o;try{r||(r=e),importScripts(r)}catch{if(r||(r=e.replace("/umd/","/esm/")),self.createFFmpegCore=(await import(r)).default,!self.createFFmpegCore)throw s}const c=r,i=a||r.replace(/.js$/g,".wasm"),p=n||r.replace(/.js$/g,".worker.js");return o=await self.createFFmpegCore({mainScriptUrlOrBlob:`${c}#${btoa(JSON.stringify({wasmURL:i,workerURL:p}))}`}),o.setLogger((e=>self.postMessage({type:t.LOG,data:e}))),o.setProgress((e=>self.postMessage({type:t.PROGRESS,data:e}))),E})(c);break;case t.EXEC:p=(({args:e,timeout:t=-1})=>{o.setTimeout(t),o.exec(...e);const r=o.ret;return o.reset(),r})(c);break;case t.WRITE_FILE:p=(({path:e,data:t})=>(o.FS.writeFile(e,t),!0))(c);break;case t.READ_FILE:p=(({path:e,encoding:t})=>o.FS.readFile(e,{encoding:t}))(c);break;case t.DELETE_FILE:p=(({path:e})=>(o.FS.unlink(e),!0))(c);break;case t.RENAME:p=(({oldPath:e,newPath:t})=>(o.FS.rename(e,t),!0))(c);break;case t.CREATE_DIR:p=(({path:e})=>(o.FS.mkdir(e),!0))(c);break;case t.LIST_DIR:p=(({path:e})=>{const t=o.FS.readdir(e),r=[];for(const a of t){const t=o.FS.stat(`${e}/${a}`),s=o.FS.isDir(t.mode);r.push({name:a,isDir:s})}return r})(c);break;case t.DELETE_DIR:p=(({path:e})=>(o.FS.rmdir(e),!0))(c);break;case t.MOUNT:p=(({fsType:e,options:t,mountPoint:r})=>{const a=e,s=o.FS.filesystems[a];return!!s&&(o.FS.mount(s,t,r),!0)})(c);break;case t.UNMOUNT:p=(({mountPoint:e})=>(o.FS.unmount(e),!0))(c);break;default:throw r}}catch(e){return void self.postMessage({id:n,type:t.ERROR,data:e.toString()})}p instanceof Uint8Array&&i.push(p.buffer),self.postMessage({id:n,type:E,data:p},i)},{}})())); +//# sourceMappingURL=814.ffmpeg.js.map \ No newline at end of file diff --git a/app/static/vendor/ffmpeg/814.ffmpeg.js.map b/app/static/vendor/ffmpeg/814.ffmpeg.js.map new file mode 100644 index 0000000..192de85 --- /dev/null +++ b/app/static/vendor/ffmpeg/814.ffmpeg.js.map @@ -0,0 +1 @@ +{"version":3,"file":"814.ffmpeg.js","mappings":"CAAA,SAA2CA,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAoB,WAAID,IAExBD,EAAiB,WAAIC,GACtB,CATD,CASGK,MAAM,I,mBCTF,MAGMC,EAAW,gEACjB,IAAIC,GACX,SAAWA,GACPA,EAAoB,KAAI,OACxBA,EAAoB,KAAI,OACxBA,EAA0B,WAAI,aAC9BA,EAAyB,UAAI,YAC7BA,EAA2B,YAAI,cAC/BA,EAAsB,OAAI,SAC1BA,EAA0B,WAAI,aAC9BA,EAAwB,SAAI,WAC5BA,EAA0B,WAAI,aAC9BA,EAAqB,MAAI,QACzBA,EAAwB,SAAI,WAC5BA,EAAwB,SAAI,WAC5BA,EAAmB,IAAI,MACvBA,EAAqB,MAAI,QACzBA,EAAuB,QAAI,SAC9B,CAhBD,CAgBGA,IAAkBA,EAAgB,CAAC,ICrB/B,MAAMC,EAA6B,IAAIC,MAAM,wBACvCC,EAAmB,IAAID,MAAM,0DAE7BE,GADmB,IAAIF,MAAM,6BACN,IAAIA,MAAM,oCCE9C,IAAIG,E,OAyFJP,KAAKQ,UAAYC,OAASC,MAAQC,KAAIC,OAAMF,KAAMG,OAC9C,MAAMC,EAAQ,GACd,IAAIJ,EACJ,IACI,GAAIE,IAASV,EAAca,OAASR,EAChC,MAAMF,EACV,OAAQO,GACJ,KAAKV,EAAca,KACfL,OAhGHD,QAASO,QAASC,EAAUC,QAASC,EAAUC,UAAWC,MACnE,MAAMC,GAASf,EACf,IACSU,IACDA,EAAWhB,GAEfsB,cAAcN,EAClB,CACA,MAMI,GALKA,IACDA,EAAWhB,EAASuB,QAAQ,QAAS,UAEzCxB,KAAKyB,wBAA0BC,OACcT,IAAWU,SACnD3B,KAAKyB,iBACN,MAAMnB,CAEd,CACA,MAAMU,EAAUC,EACVC,EAAUC,GAAsBF,EAASO,QAAQ,QAAS,SAC1DJ,EAAYC,GAEZJ,EAASO,QAAQ,QAAS,cAWhC,OAVAjB,QAAeP,KAAKyB,iBAAiB,CAGjCG,oBAAqB,GAAGZ,KAAWa,KAAKC,KAAKC,UAAU,CAAEb,UAASE,mBAEtEb,EAAOyB,WAAWtB,GAASV,KAAKiC,YAAY,CAAErB,KAAMV,EAAcgC,IAAKxB,WACvEH,EAAO4B,aAAazB,GAASV,KAAKiC,YAAY,CAC1CrB,KAAMV,EAAckC,SACpB1B,WAEGY,CAAK,EA+Dae,CAAKxB,GAClB,MACJ,KAAKX,EAAcoC,KACf5B,EAhEH,GAAG6B,OAAMC,WAAU,MAC5BjC,EAAOkC,WAAWD,GAClBjC,EAAOmC,QAAQH,GACf,MAAMI,EAAMpC,EAAOoC,IAEnB,OADApC,EAAOqC,QACAD,CAAG,EA2DSD,CAAK7B,GACZ,MACJ,KAAKX,EAAc2C,WACfnC,EA5DE,GAAGoC,OAAMpC,WACvBH,EAAOwC,GAAGC,UAAUF,EAAMpC,IACnB,GA0DYsC,CAAUnC,GACjB,MACJ,KAAKX,EAAc+C,UACfvC,EA3DC,GAAGoC,OAAMI,cAAe3C,EAAOwC,GAAGI,SAASL,EAAM,CAAEI,aA2D7CC,CAAStC,GAChB,MACJ,KAAKX,EAAckD,YACf1C,EA5DG,GAAGoC,WAClBvC,EAAOwC,GAAGM,OAAOP,IACV,GA0DYQ,CAAWzC,GAClB,MACJ,KAAKX,EAAcqD,OACf7C,EA3DD,GAAG8C,UAASC,cACvBlD,EAAOwC,GAAGW,OAAOF,EAASC,IACnB,GAyDYC,CAAO7C,GACd,MACJ,KAAKX,EAAcyD,WACfjD,EAzDE,GAAGoC,WACjBvC,EAAOwC,GAAGa,MAAMd,IACT,GAuDYe,CAAUhD,GACjB,MACJ,KAAKX,EAAc4D,SACfpD,EAxDA,GAAGoC,WACf,MAAMiB,EAAQxD,EAAOwC,GAAGiB,QAAQlB,GAC1BmB,EAAQ,GACd,IAAK,MAAMC,KAAQH,EAAO,CACtB,MAAMI,EAAO5D,EAAOwC,GAAGoB,KAAK,GAAGrB,KAAQoB,KACjCE,EAAQ7D,EAAOwC,GAAGqB,MAAMD,EAAKE,MACnCJ,EAAMK,KAAK,CAAEJ,OAAME,SACvB,CACA,OAAOH,CAAK,EAgDOM,CAAQ1D,GACf,MACJ,KAAKX,EAAcsE,WACf9D,EAhDE,GAAGoC,WACjBvC,EAAOwC,GAAG0B,MAAM3B,IACT,GA8CY4B,CAAU7D,GACjB,MACJ,KAAKX,EAAcyE,MACfjE,EA/CF,GAAGkE,SAAQC,UAASC,iBAC9B,MAAMC,EAAMH,EACNI,EAAKzE,EAAOwC,GAAGkC,YAAYF,GACjC,QAAKC,IAELzE,EAAOwC,GAAGmC,MAAMF,EAAIH,EAASC,IACtB,EAAI,EAyCQI,CAAMrE,GACb,MACJ,KAAKX,EAAciF,QACfzE,EA1CA,GAAGoE,iBACfvE,EAAOwC,GAAGqC,QAAQN,IACX,GAwCYM,CAAQvE,GACf,MACJ,QACI,MAAMV,EAElB,CACA,MAAOkF,GAMH,YALArF,KAAKiC,YAAY,CACbtB,KACAC,KAAMV,EAAcoF,MACpB5E,KAAM2E,EAAEE,YAGhB,CACI7E,aAAgB8E,YAChB1E,EAAMwD,KAAK5D,EAAK+E,QAEpBzF,KAAKiC,YAAY,CAAEtB,KAAIC,OAAMF,QAAQI,EAAM,E","sources":["webpack://FFmpegWASM/webpack/universalModuleDefinition","webpack://FFmpegWASM/./dist/esm/const.js","webpack://FFmpegWASM/./dist/esm/errors.js","webpack://FFmpegWASM/./dist/esm/worker.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"FFmpegWASM\"] = factory();\n\telse\n\t\troot[\"FFmpegWASM\"] = factory();\n})(self, () => {\nreturn ","export const MIME_TYPE_JAVASCRIPT = \"text/javascript\";\nexport const MIME_TYPE_WASM = \"application/wasm\";\nexport const CORE_VERSION = \"0.12.6\";\nexport const CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;\nexport var FFMessageType;\n(function (FFMessageType) {\n FFMessageType[\"LOAD\"] = \"LOAD\";\n FFMessageType[\"EXEC\"] = \"EXEC\";\n FFMessageType[\"WRITE_FILE\"] = \"WRITE_FILE\";\n FFMessageType[\"READ_FILE\"] = \"READ_FILE\";\n FFMessageType[\"DELETE_FILE\"] = \"DELETE_FILE\";\n FFMessageType[\"RENAME\"] = \"RENAME\";\n FFMessageType[\"CREATE_DIR\"] = \"CREATE_DIR\";\n FFMessageType[\"LIST_DIR\"] = \"LIST_DIR\";\n FFMessageType[\"DELETE_DIR\"] = \"DELETE_DIR\";\n FFMessageType[\"ERROR\"] = \"ERROR\";\n FFMessageType[\"DOWNLOAD\"] = \"DOWNLOAD\";\n FFMessageType[\"PROGRESS\"] = \"PROGRESS\";\n FFMessageType[\"LOG\"] = \"LOG\";\n FFMessageType[\"MOUNT\"] = \"MOUNT\";\n FFMessageType[\"UNMOUNT\"] = \"UNMOUNT\";\n})(FFMessageType || (FFMessageType = {}));\n","export const ERROR_UNKNOWN_MESSAGE_TYPE = new Error(\"unknown message type\");\nexport const ERROR_NOT_LOADED = new Error(\"ffmpeg is not loaded, call `await ffmpeg.load()` first\");\nexport const ERROR_TERMINATED = new Error(\"called FFmpeg.terminate()\");\nexport const ERROR_IMPORT_FAILURE = new Error(\"failed to import ffmpeg-core.js\");\n","/// \n/// \n/// \nimport { CORE_URL, FFMessageType } from \"./const.js\";\nimport { ERROR_UNKNOWN_MESSAGE_TYPE, ERROR_NOT_LOADED, ERROR_IMPORT_FAILURE, } from \"./errors.js\";\nlet ffmpeg;\nconst load = async ({ coreURL: _coreURL, wasmURL: _wasmURL, workerURL: _workerURL, }) => {\n const first = !ffmpeg;\n try {\n if (!_coreURL)\n _coreURL = CORE_URL;\n // when web worker type is `classic`.\n importScripts(_coreURL);\n }\n catch {\n if (!_coreURL)\n _coreURL = CORE_URL.replace('/umd/', '/esm/');\n // when web worker type is `module`.\n self.createFFmpegCore = (await import(\n /* webpackIgnore: true */ /* @vite-ignore */ _coreURL)).default;\n if (!self.createFFmpegCore) {\n throw ERROR_IMPORT_FAILURE;\n }\n }\n const coreURL = _coreURL;\n const wasmURL = _wasmURL ? _wasmURL : _coreURL.replace(/.js$/g, \".wasm\");\n const workerURL = _workerURL\n ? _workerURL\n : _coreURL.replace(/.js$/g, \".worker.js\");\n ffmpeg = await self.createFFmpegCore({\n // Fix `Overload resolution failed.` when using multi-threaded ffmpeg-core.\n // Encoded wasmURL and workerURL in the URL as a hack to fix locateFile issue.\n mainScriptUrlOrBlob: `${coreURL}#${btoa(JSON.stringify({ wasmURL, workerURL }))}`,\n });\n ffmpeg.setLogger((data) => self.postMessage({ type: FFMessageType.LOG, data }));\n ffmpeg.setProgress((data) => self.postMessage({\n type: FFMessageType.PROGRESS,\n data,\n }));\n return first;\n};\nconst exec = ({ args, timeout = -1 }) => {\n ffmpeg.setTimeout(timeout);\n ffmpeg.exec(...args);\n const ret = ffmpeg.ret;\n ffmpeg.reset();\n return ret;\n};\nconst writeFile = ({ path, data }) => {\n ffmpeg.FS.writeFile(path, data);\n return true;\n};\nconst readFile = ({ path, encoding }) => ffmpeg.FS.readFile(path, { encoding });\n// TODO: check if deletion works.\nconst deleteFile = ({ path }) => {\n ffmpeg.FS.unlink(path);\n return true;\n};\nconst rename = ({ oldPath, newPath }) => {\n ffmpeg.FS.rename(oldPath, newPath);\n return true;\n};\n// TODO: check if creation works.\nconst createDir = ({ path }) => {\n ffmpeg.FS.mkdir(path);\n return true;\n};\nconst listDir = ({ path }) => {\n const names = ffmpeg.FS.readdir(path);\n const nodes = [];\n for (const name of names) {\n const stat = ffmpeg.FS.stat(`${path}/${name}`);\n const isDir = ffmpeg.FS.isDir(stat.mode);\n nodes.push({ name, isDir });\n }\n return nodes;\n};\n// TODO: check if deletion works.\nconst deleteDir = ({ path }) => {\n ffmpeg.FS.rmdir(path);\n return true;\n};\nconst mount = ({ fsType, options, mountPoint }) => {\n const str = fsType;\n const fs = ffmpeg.FS.filesystems[str];\n if (!fs)\n return false;\n ffmpeg.FS.mount(fs, options, mountPoint);\n return true;\n};\nconst unmount = ({ mountPoint }) => {\n ffmpeg.FS.unmount(mountPoint);\n return true;\n};\nself.onmessage = async ({ data: { id, type, data: _data }, }) => {\n const trans = [];\n let data;\n try {\n if (type !== FFMessageType.LOAD && !ffmpeg)\n throw ERROR_NOT_LOADED; // eslint-disable-line\n switch (type) {\n case FFMessageType.LOAD:\n data = await load(_data);\n break;\n case FFMessageType.EXEC:\n data = exec(_data);\n break;\n case FFMessageType.WRITE_FILE:\n data = writeFile(_data);\n break;\n case FFMessageType.READ_FILE:\n data = readFile(_data);\n break;\n case FFMessageType.DELETE_FILE:\n data = deleteFile(_data);\n break;\n case FFMessageType.RENAME:\n data = rename(_data);\n break;\n case FFMessageType.CREATE_DIR:\n data = createDir(_data);\n break;\n case FFMessageType.LIST_DIR:\n data = listDir(_data);\n break;\n case FFMessageType.DELETE_DIR:\n data = deleteDir(_data);\n break;\n case FFMessageType.MOUNT:\n data = mount(_data);\n break;\n case FFMessageType.UNMOUNT:\n data = unmount(_data);\n break;\n default:\n throw ERROR_UNKNOWN_MESSAGE_TYPE;\n }\n }\n catch (e) {\n self.postMessage({\n id,\n type: FFMessageType.ERROR,\n data: e.toString(),\n });\n return;\n }\n if (data instanceof Uint8Array) {\n trans.push(data.buffer);\n }\n self.postMessage({ id, type, data }, trans);\n};\n"],"names":["root","factory","exports","module","define","amd","self","CORE_URL","FFMessageType","ERROR_UNKNOWN_MESSAGE_TYPE","Error","ERROR_NOT_LOADED","ERROR_IMPORT_FAILURE","ffmpeg","onmessage","async","data","id","type","_data","trans","LOAD","coreURL","_coreURL","wasmURL","_wasmURL","workerURL","_workerURL","first","importScripts","replace","createFFmpegCore","import","default","mainScriptUrlOrBlob","btoa","JSON","stringify","setLogger","postMessage","LOG","setProgress","PROGRESS","load","EXEC","args","timeout","setTimeout","exec","ret","reset","WRITE_FILE","path","FS","writeFile","READ_FILE","encoding","readFile","DELETE_FILE","unlink","deleteFile","RENAME","oldPath","newPath","rename","CREATE_DIR","mkdir","createDir","LIST_DIR","names","readdir","nodes","name","stat","isDir","mode","push","listDir","DELETE_DIR","rmdir","deleteDir","MOUNT","fsType","options","mountPoint","str","fs","filesystems","mount","UNMOUNT","unmount","e","ERROR","toString","Uint8Array","buffer"],"sourceRoot":""} \ No newline at end of file diff --git a/app/static/vendor/ffmpeg/ffmpeg-core.js b/app/static/vendor/ffmpeg/ffmpeg-core.js new file mode 100644 index 0000000..72b2366 --- /dev/null +++ b/app/static/vendor/ffmpeg/ffmpeg-core.js @@ -0,0 +1,21 @@ + +var createFFmpegCore = (() => { + var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined; + if (typeof __filename !== 'undefined') _scriptDir = _scriptDir || __filename; + return ( +function(createFFmpegCore = {}) { + +var Module=typeof createFFmpegCore!="undefined"?createFFmpegCore:{};var readyPromiseResolve,readyPromiseReject;Module["ready"]=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});const NULL=0;const SIZE_I32=Uint32Array.BYTES_PER_ELEMENT;const DEFAULT_ARGS=["./ffmpeg","-nostdin","-y"];Module["NULL"]=NULL;Module["SIZE_I32"]=SIZE_I32;Module["DEFAULT_ARGS"]=DEFAULT_ARGS;Module["ret"]=-1;Module["timeout"]=-1;Module["logger"]=()=>{};Module["progress"]=()=>{};function stringToPtr(str){const len=Module["lengthBytesUTF8"](str)+1;const ptr=Module["_malloc"](len);Module["stringToUTF8"](str,ptr,len);return ptr}function stringsToPtr(strs){const len=strs.length;const ptr=Module["_malloc"](len*SIZE_I32);for(let i=0;i{throw toThrow};var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");if(ENVIRONMENT_IS_WORKER){scriptDirectory=nodePath.dirname(scriptDirectory)+"/"}else{scriptDirectory=__dirname+"/"}read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)onerror(err);else onload(binary?data.buffer:data)})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow};Module["inspect"]=()=>"[Emscripten Module object]"}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}setWindowTitle=title=>document.title=title}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort(text)}}var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b);Module["HEAP64"]=HEAP64=new BigInt64Array(b);Module["HEAPU64"]=HEAPU64=new BigUint64Array(b)}var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeKeepaliveCounter=0;function keepRuntimeAlive(){return noExitRuntime||runtimeKeepaliveCounter>0}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.init.initialized)FS.init();FS.ignorePermissions=false;TTY.init();SOCKFS.root=FS.mount(SOCKFS,{},null);callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return filename.startsWith(dataURIPrefix)}function isFileURI(filename){return filename.startsWith("file://")}var wasmBinaryFile;wasmBinaryFile="ffmpeg-core.wasm";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinary(file){try{if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}catch(err){abort(err)}}function getBinaryPromise(binaryFile){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch=="function"&&!isFileURI(binaryFile)){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{if(!response["ok"]){throw"failed to load wasm binary file at '"+binaryFile+"'"}return response["arrayBuffer"]()}).catch(()=>getBinary(binaryFile))}else{if(readAsync){return new Promise((resolve,reject)=>{readAsync(binaryFile,response=>resolve(new Uint8Array(response)),reject)})}}}return Promise.resolve().then(()=>getBinary(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>{return WebAssembly.instantiate(binary,imports)}).then(instance=>{return instance}).then(receiver,reason=>{err("failed to asynchronously prepare wasm: "+reason);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err("wasm streaming compile failed: "+reason);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}else{return instantiateArrayBuffer(binaryFile,imports,callback)}}function createWasm(){var info={"a":wasmImports};function receiveInstance(instance,module){var exports=instance.exports;Module["asm"]=exports;wasmMemory=Module["asm"]["ra"];updateMemoryViews();wasmTable=Module["asm"]["ua"];addOnInit(Module["asm"]["sa"]);removeRunDependency("wasm-instantiate");return exports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err("Module.instantiateWasm callback failed with error: "+e);readyPromiseReject(e)}}instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var ASM_CONSTS={6059608:$0=>{Module.ret=$0}};function send_progress(progress,time){Module.receiveProgress(progress,time)}function is_timeout(diff){if(Module.timeout===-1)return 0;else{return Module.timeout<=diff}}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}function callRuntimeCallbacks(callbacks){while(callbacks.length>0){callbacks.shift()(Module)}}var wasmTableMirror=[];function getWasmTableEntry(funcPtr){var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func}function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr>>0];case"i8":return HEAP8[ptr>>0];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP64[ptr>>3];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr>>0]=value;break;case"i8":HEAP8[ptr>>0]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":HEAP64[ptr>>3]=BigInt(value);break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(heapOrArray,idx,maxBytesToRead){var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str}function UTF8ToString(ptr,maxBytesToRead){return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""}function ___assert_fail(condition,filename,line,func){abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])}function ExceptionInfo(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24;this.set_type=function(type){HEAPU32[this.ptr+4>>2]=type};this.get_type=function(){return HEAPU32[this.ptr+4>>2]};this.set_destructor=function(destructor){HEAPU32[this.ptr+8>>2]=destructor};this.get_destructor=function(){return HEAPU32[this.ptr+8>>2]};this.set_caught=function(caught){caught=caught?1:0;HEAP8[this.ptr+12>>0]=caught};this.get_caught=function(){return HEAP8[this.ptr+12>>0]!=0};this.set_rethrown=function(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13>>0]=rethrown};this.get_rethrown=function(){return HEAP8[this.ptr+13>>0]!=0};this.init=function(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)};this.set_adjusted_ptr=function(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr};this.get_adjusted_ptr=function(){return HEAPU32[this.ptr+16>>2]};this.get_exception_ptr=function(){var isPointer=___cxa_is_pointer_type(this.get_type());if(isPointer){return HEAPU32[this.excPtr>>2]}var adjusted=this.get_adjusted_ptr();if(adjusted!==0)return adjusted;return this.excPtr}}var exceptionLast=0;var uncaughtExceptionCount=0;function ___cxa_throw(ptr,type,destructor){var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast}var dlopenMissingError="To use dlopen, you need enable dynamic linking, see https://emscripten.org/docs/compiling/Dynamic-Linking.html";function ___dlsym(handle,symbol){abort(dlopenMissingError)}var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:function(){var paths=Array.prototype.slice.call(arguments);return PATH.normalize(paths.join("/"))},join2:(l,r)=>{return PATH.normalize(l+"/"+r)}};function initRandomFill(){if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("initRandomDevice")}function randomFill(view){return(randomFill=initRandomFill())(view)}var PATH_FS={resolve:function(){var resolvedPath="",resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?arguments[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len}function stringToUTF8Array(str,heap,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx}function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var TTY={ttys:[],init:function(){},shutdown:function(){},register:function(dev,ops){TTY.ttys[dev]={input:[],output:[],ops:ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open:function(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close:function(stream){stream.tty.ops.fsync(stream.tty)},fsync:function(stream){stream.tty.ops.fsync(stream.tty)},read:function(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){result=buf.slice(0,bytesRead).toString("utf-8")}else{result=null}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else if(typeof readline=="function"){result=readline();if(result!==null){result+="\n"}}if(!result){return null}tty.input=intArrayFromString(result,true)}return tty.input.shift()},put_char:function(tty,val){if(val===null||val===10){out(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync:function(tty){if(tty.output&&tty.output.length>0){out(UTF8ArrayToString(tty.output,0));tty.output=[]}}},default_tty1_ops:{put_char:function(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync:function(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output,0));tty.output=[]}}}};function zeroMemory(address,size){HEAPU8.fill(0,address,address+size);return address}function alignMemory(size,alignment){return Math.ceil(size/alignment)*alignment}function mmapAlloc(size){size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(!ptr)return 0;return zeroMemory(ptr,size)}var MEMFS={ops_table:null,mount:function(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode:function(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}if(!MEMFS.ops_table){MEMFS.ops_table={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}}}var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray:function(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage:function(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage:function(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr:function(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr:function(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup:function(parent,name){throw FS.genericErrors[44]},mknod:function(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename:function(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp;old_node.parent=new_dir},unlink:function(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir:function(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir:function(node){var entries=[".",".."];for(var key in node.contents){if(!node.contents.hasOwnProperty(key)){continue}entries.push(key)}return entries},symlink:function(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink:function(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read:function(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{assert(arrayBuffer,`Loading data file "${url}" failed (no arrayBuffer).`);onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},event=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)}var preloadPlugins=Module["preloadPlugins"]||[];function FS_handledByPreloadPlugin(byteArray,fullname,finish,onerror){if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(function(plugin){if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled}function FS_createPreloadedFile(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish){var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){if(preFinish)preFinish();if(!dontCreateFile){FS.createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}if(onload)onload();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{if(onerror)onerror();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,byteArray=>processData(byteArray),onerror)}else{processData(url)}}function FS_modeStringToFlags(str){var flagModes={"r":0,"r+":2,"w":512|64|1,"w+":512|64|2,"a":1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags}function FS_getMode(canRead,canWrite){var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode}var WORKERFS={DIR_MODE:16895,FILE_MODE:33279,reader:null,mount:function(mount){assert(ENVIRONMENT_IS_WORKER);if(!WORKERFS.reader)WORKERFS.reader=new FileReaderSync;var root=WORKERFS.createNode(null,"/",WORKERFS.DIR_MODE,0);var createdParents={};function ensureParent(path){var parts=path.split("/");var parent=root;for(var i=0;i=stream.node.size)return 0;var chunk=stream.node.contents.slice(position,position+length);var ab=WORKERFS.reader.readAsArrayBuffer(chunk);buffer.set(new Uint8Array(ab),offset);return chunk.size},write:function(stream,buffer,offset,length,position){throw new FS.ErrnoError(29)},llseek:function(stream,offset,whence){var position=offset;if(whence===1){position+=stream.position}else if(whence===2){if(FS.isFile(stream.node.mode)){position+=stream.node.size}}if(position<0){throw new FS.ErrnoError(28)}return position}}};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:null,genericErrors:{},filesystems:null,syncFSRequests:0,lookupPath:(path,opts={})=>{path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath:node=>{var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName:(parentid,name)=>{var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode:node=>{var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode:node=>{var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode:(parent,name)=>{var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode,parent)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode:(parent,name,mode,rdev)=>{var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode:node=>{FS.hashRemoveNode(node)},isRoot:node=>{return node===node.parent},isMountpoint:node=>{return!!node.mounted},isFile:mode=>{return(mode&61440)===32768},isDir:mode=>{return(mode&61440)===16384},isLink:mode=>{return(mode&61440)===40960},isChrdev:mode=>{return(mode&61440)===8192},isBlkdev:mode=>{return(mode&61440)===24576},isFIFO:mode=>{return(mode&61440)===4096},isSocket:mode=>{return(mode&49152)===49152},flagsToPermissionString:flag=>{var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions:(node,perms)=>{if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup:dir=>{var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate:(dir,name)=>{try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete:(dir,name,isdir)=>{var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen:(node,flags)=>{if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd:()=>{for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStream:fd=>FS.streams[fd],createStream:(stream,fd=-1)=>{if(!FS.FSStream){FS.FSStream=function(){this.shared={}};FS.FSStream.prototype={};Object.defineProperties(FS.FSStream.prototype,{object:{get:function(){return this.node},set:function(val){this.node=val}},isRead:{get:function(){return(this.flags&2097155)!==1}},isWrite:{get:function(){return(this.flags&2097155)!==0}},isAppend:{get:function(){return this.flags&1024}},flags:{get:function(){return this.shared.flags},set:function(val){this.shared.flags=val}},position:{get:function(){return this.shared.position},set:function(val){this.shared.position=val}}})}stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream:fd=>{FS.streams[fd]=null},chrdev_stream_ops:{open:stream=>{var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;if(stream.stream_ops.open){stream.stream_ops.open(stream)}},llseek:()=>{throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice:(dev,ops)=>{FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts:mount=>{var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push.apply(check,m.mounts)}return mounts},syncfs:(populate,callback)=>{if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount:(type,opts,mountpoint)=>{var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type:type,opts:opts,mountpoint:mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount:mountpoint=>{var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup:(parent,name)=>{return parent.node_ops.lookup(parent,name)},mknod:(path,mode,dev)=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create:(path,mode)=>{mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir:(path,mode)=>{mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree:(path,mode)=>{var dirs=path.split("/");var d="";for(var i=0;i{if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink:(oldpath,newpath)=>{if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename:(old_path,new_path)=>{var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name)}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir:path=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir:path=>{var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node.node_ops.readdir){throw new FS.ErrnoError(54)}return node.node_ops.readdir(node)},unlink:path=>{var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink:path=>{var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return PATH_FS.resolve(FS.getPath(link.parent),link.node_ops.readlink(link))},stat:(path,dontFollow)=>{var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;if(!node){throw new FS.ErrnoError(44)}if(!node.node_ops.getattr){throw new FS.ErrnoError(63)}return node.node_ops.getattr(node)},lstat:path=>{return FS.stat(path,true)},chmod:(path,mode,dontFollow)=>{var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}node.node_ops.setattr(node,{mode:mode&4095|node.mode&~4095,timestamp:Date.now()})},lchmod:(path,mode)=>{FS.chmod(path,mode,true)},fchmod:(fd,mode)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}FS.chmod(stream.node,mode)},chown:(path,uid,gid,dontFollow)=>{var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}node.node_ops.setattr(node,{timestamp:Date.now()})},lchown:(path,uid,gid)=>{FS.chown(path,uid,gid,true)},fchown:(fd,uid,gid)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}FS.chown(stream.node,uid,gid)},truncate:(path,len)=>{if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}if(!node.node_ops.setattr){throw new FS.ErrnoError(63)}if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}node.node_ops.setattr(node,{size:len,timestamp:Date.now()})},ftruncate:(fd,len)=>{var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.truncate(stream.node,len)},utime:(path,atime,mtime)=>{var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;node.node_ops.setattr(node,{timestamp:Math.max(atime,mtime)})},open:(path,flags,mode)=>{if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;mode=typeof mode=="undefined"?438:mode;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;if(typeof path=="object"){node=path}else{path=PATH.normalize(path);try{var lookup=FS.lookupPath(path,{follow:!(flags&131072)});node=lookup.node}catch(e){}}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else{node=FS.mknod(path,mode,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node:node,path:FS.getPath(node),flags:flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(Module["logReadFiles"]&&!(flags&1)){if(!FS.readFiles)FS.readFiles={};if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close:stream=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed:stream=>{return stream.fd===null},llseek:(stream,offset,whence)=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read:(stream,buffer,offset,length,position)=>{if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write:(stream,buffer,offset,length,position,canOwn)=>{if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},allocate:(stream,offset,length)=>{if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(offset<0||length<=0){throw new FS.ErrnoError(28)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(!FS.isFile(stream.node.mode)&&!FS.isDir(stream.node.mode)){throw new FS.ErrnoError(43)}if(!stream.stream_ops.allocate){throw new FS.ErrnoError(138)}stream.stream_ops.allocate(stream,offset,length)},mmap:(stream,length,position,prot,flags)=>{if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync:(stream,buffer,offset,length,mmapFlags)=>{if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},munmap:stream=>0,ioctl:(stream,cmd,arg)=>{if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile:(path,opts={})=>{opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf,0)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile:(path,data,opts={})=>{opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir:path=>{var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories:()=>{FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices:()=>{FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories:()=>{FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount:()=>{var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup:(parent,name)=>{var fd=+name;var stream=FS.getStream(fd);if(!stream)throw new FS.ErrnoError(8);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams:()=>{if(Module["stdin"]){FS.createDevice("/dev","stdin",Module["stdin"])}else{FS.symlink("/dev/tty","/dev/stdin")}if(Module["stdout"]){FS.createDevice("/dev","stdout",null,Module["stdout"])}else{FS.symlink("/dev/tty","/dev/stdout")}if(Module["stderr"]){FS.createDevice("/dev","stderr",null,Module["stderr"])}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},ensureErrnoError:()=>{if(FS.ErrnoError)return;FS.ErrnoError=function ErrnoError(errno,node){this.name="ErrnoError";this.node=node;this.setErrno=function(errno){this.errno=errno};this.setErrno(errno);this.message="FS error"};FS.ErrnoError.prototype=new Error;FS.ErrnoError.prototype.constructor=FS.ErrnoError;[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""})},staticInit:()=>{FS.ensureErrnoError();FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={"MEMFS":MEMFS,"WORKERFS":WORKERFS}},init:(input,output,error)=>{FS.init.initialized=true;FS.ensureErrnoError();Module["stdin"]=input||Module["stdin"];Module["stdout"]=output||Module["stdout"];Module["stderr"]=error||Module["stderr"];FS.createStandardStreams()},quit:()=>{FS.init.initialized=false;for(var i=0;i{var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath:(path,dontResolveLastLink)=>{try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath:(parent,path,canRead,canWrite)=>{parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){}parent=current}return current},createFile:(parent,name,properties,canRead,canWrite)=>{var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile:(parent,name,data,canRead,canWrite,canOwn)=>{var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;i{var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(!!input,!!output);if(!FS.createDevice.major)FS.createDevice.major=64;var dev=FS.makedev(FS.createDevice.major++,0);FS.registerDevice(dev,{open:stream=>{stream.seekable=false},close:stream=>{if(output&&output.buffer&&output.buffer.length){output(10)}},read:(stream,buffer,offset,length,pos)=>{var bytesRead=0;for(var i=0;i{for(var i=0;i{if(obj.isDevice||obj.isFolder||obj.link||obj.contents)return true;if(typeof XMLHttpRequest!="undefined"){throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread.")}else if(read_){try{obj.contents=intArrayFromString(read_(obj.url),true);obj.usedBytes=obj.contents.length}catch(e){throw new FS.ErrnoError(29)}}else{throw new Error("Cannot load without read() or XMLHttpRequest.")}},createLazyFile:(parent,name,url,canRead,canWrite)=>{function LazyUint8Array(){this.lengthKnown=false;this.chunks=[]}LazyUint8Array.prototype.get=function LazyUint8Array_get(idx){if(idx>this.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]};LazyUint8Array.prototype.setDataGetter=function LazyUint8Array_setDataGetter(getter){this.getter=getter};LazyUint8Array.prototype.cacheLength=function LazyUint8Array_cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true};if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;Object.defineProperties(lazyArray,{length:{get:function(){if(!this.lengthKnown){this.cacheLength()}return this._length}},chunkSize:{get:function(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}});var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url:url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=function forceLoadLazyFile(){FS.forceLoadFile(node);return fn.apply(null,arguments)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr:ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt:function(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat:function(func,path,buf){try{var stat=func(path)}catch(e){if(e&&e.node&&PATH.normalize(path)!==PATH.normalize(FS.getPath(e.node))){return-54}throw e}HEAP32[buf>>2]=stat.dev;HEAP32[buf+8>>2]=stat.ino;HEAP32[buf+12>>2]=stat.mode;HEAPU32[buf+16>>2]=stat.nlink;HEAP32[buf+20>>2]=stat.uid;HEAP32[buf+24>>2]=stat.gid;HEAP32[buf+28>>2]=stat.rdev;HEAP64[buf+40>>3]=BigInt(stat.size);HEAP32[buf+48>>2]=4096;HEAP32[buf+52>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+56>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+64>>2]=atime%1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+80>>2]=mtime%1e3*1e3;HEAP64[buf+88>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+96>>2]=ctime%1e3*1e3;HEAP64[buf+104>>3]=BigInt(stat.ino);return 0},doMsync:function(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},varargs:undefined,get:function(){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret},getStr:function(ptr){var ret=UTF8ToString(ptr);return ret},getStreamFromFD:function(fd){var stream=FS.getStream(fd);if(!stream)throw new FS.ErrnoError(8);return stream}};function ___syscall__newselect(nfds,readfds,writefds,exceptfds,timeout){try{var total=0;var srcReadLow=readfds?HEAP32[readfds>>2]:0,srcReadHigh=readfds?HEAP32[readfds+4>>2]:0;var srcWriteLow=writefds?HEAP32[writefds>>2]:0,srcWriteHigh=writefds?HEAP32[writefds+4>>2]:0;var srcExceptLow=exceptfds?HEAP32[exceptfds>>2]:0,srcExceptHigh=exceptfds?HEAP32[exceptfds+4>>2]:0;var dstReadLow=0,dstReadHigh=0;var dstWriteLow=0,dstWriteHigh=0;var dstExceptLow=0,dstExceptHigh=0;var allLow=(readfds?HEAP32[readfds>>2]:0)|(writefds?HEAP32[writefds>>2]:0)|(exceptfds?HEAP32[exceptfds>>2]:0);var allHigh=(readfds?HEAP32[readfds+4>>2]:0)|(writefds?HEAP32[writefds+4>>2]:0)|(exceptfds?HEAP32[exceptfds+4>>2]:0);var check=function(fd,low,high,val){return fd<32?low&val:high&val};for(var fd=0;fd>2]=dstReadLow;HEAP32[readfds+4>>2]=dstReadHigh}if(writefds){HEAP32[writefds>>2]=dstWriteLow;HEAP32[writefds+4>>2]=dstWriteHigh}if(exceptfds){HEAP32[exceptfds>>2]=dstExceptLow;HEAP32[exceptfds+4>>2]=dstExceptHigh}return total}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var SOCKFS={mount:function(mount){Module["websocket"]=Module["websocket"]&&"object"===typeof Module["websocket"]?Module["websocket"]:{};Module["websocket"]._callbacks={};Module["websocket"]["on"]=function(event,callback){if("function"===typeof callback){this._callbacks[event]=callback}return this};Module["websocket"].emit=function(event,param){if("function"===typeof this._callbacks[event]){this._callbacks[event].call(this,param)}};return FS.createNode(null,"/",16384|511,0)},createSocket:function(family,type,protocol){type&=~526336;var streaming=type==1;if(streaming&&protocol&&protocol!=6){throw new FS.ErrnoError(66)}var sock={family:family,type:type,protocol:protocol,server:null,error:null,peers:{},pending:[],recv_queue:[],sock_ops:SOCKFS.websocket_sock_ops};var name=SOCKFS.nextname();var node=FS.createNode(SOCKFS.root,name,49152,0);node.sock=sock;var stream=FS.createStream({path:name,node:node,flags:2,seekable:false,stream_ops:SOCKFS.stream_ops});sock.stream=stream;return sock},getSocket:function(fd){var stream=FS.getStream(fd);if(!stream||!FS.isSocket(stream.node.mode)){return null}return stream.node.sock},stream_ops:{poll:function(stream){var sock=stream.node.sock;return sock.sock_ops.poll(sock)},ioctl:function(stream,request,varargs){var sock=stream.node.sock;return sock.sock_ops.ioctl(sock,request,varargs)},read:function(stream,buffer,offset,length,position){var sock=stream.node.sock;var msg=sock.sock_ops.recvmsg(sock,length);if(!msg){return 0}buffer.set(msg.buffer,offset);return msg.buffer.length},write:function(stream,buffer,offset,length,position){var sock=stream.node.sock;return sock.sock_ops.sendmsg(sock,buffer,offset,length)},close:function(stream){var sock=stream.node.sock;sock.sock_ops.close(sock)}},nextname:function(){if(!SOCKFS.nextname.current){SOCKFS.nextname.current=0}return"socket["+SOCKFS.nextname.current+++"]"},websocket_sock_ops:{createPeer:function(sock,addr,port){var ws;if(typeof addr=="object"){ws=addr;addr=null;port=null}if(ws){if(ws._socket){addr=ws._socket.remoteAddress;port=ws._socket.remotePort}else{var result=/ws[s]?:\/\/([^:]+):(\d+)/.exec(ws.url);if(!result){throw new Error("WebSocket URL must be in the format ws(s)://address:port")}addr=result[1];port=parseInt(result[2],10)}}else{try{var runtimeConfig=Module["websocket"]&&"object"===typeof Module["websocket"];var url="ws:#".replace("#","//");if(runtimeConfig){if("string"===typeof Module["websocket"]["url"]){url=Module["websocket"]["url"]}}if(url==="ws://"||url==="wss://"){var parts=addr.split("/");url=url+parts[0]+":"+port+"/"+parts.slice(1).join("/")}var subProtocols="binary";if(runtimeConfig){if("string"===typeof Module["websocket"]["subprotocol"]){subProtocols=Module["websocket"]["subprotocol"]}}var opts=undefined;if(subProtocols!=="null"){subProtocols=subProtocols.replace(/^ +| +$/g,"").split(/ *, */);opts=subProtocols}if(runtimeConfig&&null===Module["websocket"]["subprotocol"]){subProtocols="null";opts=undefined}var WebSocketConstructor;if(ENVIRONMENT_IS_NODE){WebSocketConstructor=require("ws")}else{WebSocketConstructor=WebSocket}ws=new WebSocketConstructor(url,opts);ws.binaryType="arraybuffer"}catch(e){throw new FS.ErrnoError(23)}}var peer={addr:addr,port:port,socket:ws,dgram_send_queue:[]};SOCKFS.websocket_sock_ops.addPeer(sock,peer);SOCKFS.websocket_sock_ops.handlePeerEvents(sock,peer);if(sock.type===2&&typeof sock.sport!="undefined"){peer.dgram_send_queue.push(new Uint8Array([255,255,255,255,"p".charCodeAt(0),"o".charCodeAt(0),"r".charCodeAt(0),"t".charCodeAt(0),(sock.sport&65280)>>8,sock.sport&255]))}return peer},getPeer:function(sock,addr,port){return sock.peers[addr+":"+port]},addPeer:function(sock,peer){sock.peers[peer.addr+":"+peer.port]=peer},removePeer:function(sock,peer){delete sock.peers[peer.addr+":"+peer.port]},handlePeerEvents:function(sock,peer){var first=true;var handleOpen=function(){Module["websocket"].emit("open",sock.stream.fd);try{var queued=peer.dgram_send_queue.shift();while(queued){peer.socket.send(queued);queued=peer.dgram_send_queue.shift()}}catch(e){peer.socket.close()}};function handleMessage(data){if(typeof data=="string"){var encoder=new TextEncoder;data=encoder.encode(data)}else{assert(data.byteLength!==undefined);if(data.byteLength==0){return}data=new Uint8Array(data)}var wasfirst=first;first=false;if(wasfirst&&data.length===10&&data[0]===255&&data[1]===255&&data[2]===255&&data[3]===255&&data[4]==="p".charCodeAt(0)&&data[5]==="o".charCodeAt(0)&&data[6]==="r".charCodeAt(0)&&data[7]==="t".charCodeAt(0)){var newport=data[8]<<8|data[9];SOCKFS.websocket_sock_ops.removePeer(sock,peer);peer.port=newport;SOCKFS.websocket_sock_ops.addPeer(sock,peer);return}sock.recv_queue.push({addr:peer.addr,port:peer.port,data:data});Module["websocket"].emit("message",sock.stream.fd)}if(ENVIRONMENT_IS_NODE){peer.socket.on("open",handleOpen);peer.socket.on("message",function(data,isBinary){if(!isBinary){return}handleMessage(new Uint8Array(data).buffer)});peer.socket.on("close",function(){Module["websocket"].emit("close",sock.stream.fd)});peer.socket.on("error",function(error){sock.error=14;Module["websocket"].emit("error",[sock.stream.fd,sock.error,"ECONNREFUSED: Connection refused"])})}else{peer.socket.onopen=handleOpen;peer.socket.onclose=function(){Module["websocket"].emit("close",sock.stream.fd)};peer.socket.onmessage=function peer_socket_onmessage(event){handleMessage(event.data)};peer.socket.onerror=function(error){sock.error=14;Module["websocket"].emit("error",[sock.stream.fd,sock.error,"ECONNREFUSED: Connection refused"])}}},poll:function(sock){if(sock.type===1&&sock.server){return sock.pending.length?64|1:0}var mask=0;var dest=sock.type===1?SOCKFS.websocket_sock_ops.getPeer(sock,sock.daddr,sock.dport):null;if(sock.recv_queue.length||!dest||dest&&dest.socket.readyState===dest.socket.CLOSING||dest&&dest.socket.readyState===dest.socket.CLOSED){mask|=64|1}if(!dest||dest&&dest.socket.readyState===dest.socket.OPEN){mask|=4}if(dest&&dest.socket.readyState===dest.socket.CLOSING||dest&&dest.socket.readyState===dest.socket.CLOSED){mask|=16}return mask},ioctl:function(sock,request,arg){switch(request){case 21531:var bytes=0;if(sock.recv_queue.length){bytes=sock.recv_queue[0].data.length}HEAP32[arg>>2]=bytes;return 0;default:return 28}},close:function(sock){if(sock.server){try{sock.server.close()}catch(e){}sock.server=null}var peers=Object.keys(sock.peers);for(var i=0;i>2]=value;return value}function inetPton4(str){var b=str.split(".");for(var i=0;i<4;i++){var tmp=Number(b[i]);if(isNaN(tmp))return null;b[i]=tmp}return(b[0]|b[1]<<8|b[2]<<16|b[3]<<24)>>>0}function jstoi_q(str){return parseInt(str)}function inetPton6(str){var words;var w,offset,z;var valid6regx=/^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i;var parts=[];if(!valid6regx.test(str)){return null}if(str==="::"){return[0,0,0,0,0,0,0,0]}if(str.startsWith("::")){str=str.replace("::","Z:")}else{str=str.replace("::",":Z:")}if(str.indexOf(".")>0){str=str.replace(new RegExp("[.]","g"),":");words=str.split(":");words[words.length-4]=jstoi_q(words[words.length-4])+jstoi_q(words[words.length-3])*256;words[words.length-3]=jstoi_q(words[words.length-2])+jstoi_q(words[words.length-1])*256;words=words.slice(0,words.length-2)}else{words=str.split(":")}offset=0;z=0;for(w=0;w>2]=16}HEAP16[sa>>1]=family;HEAP32[sa+4>>2]=addr;HEAP16[sa+2>>1]=_htons(port);break;case 10:addr=inetPton6(addr);zeroMemory(sa,28);if(addrlen){HEAP32[addrlen>>2]=28}HEAP32[sa>>2]=family;HEAP32[sa+8>>2]=addr[0];HEAP32[sa+12>>2]=addr[1];HEAP32[sa+16>>2]=addr[2];HEAP32[sa+20>>2]=addr[3];HEAP16[sa+2>>1]=_htons(port);break;default:return 5}return 0}var DNS={address_map:{id:1,addrs:{},names:{}},lookup_name:function(name){var res=inetPton4(name);if(res!==null){return name}res=inetPton6(name);if(res!==null){return name}var addr;if(DNS.address_map.addrs[name]){addr=DNS.address_map.addrs[name]}else{var id=DNS.address_map.id++;assert(id<65535,"exceeded max address mappings of 65535");addr="172.29."+(id&255)+"."+(id&65280);DNS.address_map.names[addr]=name;DNS.address_map.addrs[name]=addr}return addr},lookup_addr:function(addr){if(DNS.address_map.names[addr]){return DNS.address_map.names[addr]}return null}};function ___syscall_accept4(fd,addr,addrlen,flags,d1,d2){try{var sock=getSocketFromFD(fd);var newsock=sock.sock_ops.accept(sock);if(addr){var errno=writeSockaddr(addr,newsock.family,DNS.lookup_name(newsock.daddr),newsock.dport,addrlen)}return newsock.stream.fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function inetNtop4(addr){return(addr&255)+"."+(addr>>8&255)+"."+(addr>>16&255)+"."+(addr>>24&255)}function inetNtop6(ints){var str="";var word=0;var longest=0;var lastzero=0;var zstart=0;var len=0;var i=0;var parts=[ints[0]&65535,ints[0]>>16,ints[1]&65535,ints[1]>>16,ints[2]&65535,ints[2]>>16,ints[3]&65535,ints[3]>>16];var hasipv4=true;var v4part="";for(i=0;i<5;i++){if(parts[i]!==0){hasipv4=false;break}}if(hasipv4){v4part=inetNtop4(parts[6]|parts[7]<<16);if(parts[5]===-1){str="::ffff:";str+=v4part;return str}if(parts[5]===0){str="::";if(v4part==="0.0.0.0")v4part="";if(v4part==="0.0.0.1")v4part="1";str+=v4part;return str}}for(word=0;word<8;word++){if(parts[word]===0){if(word-lastzero>1){len=0}lastzero=word;len++}if(len>longest){longest=len;zstart=word-longest+1}}for(word=0;word<8;word++){if(longest>1){if(parts[word]===0&&word>=zstart&&word>1];var port=_ntohs(HEAPU16[sa+2>>1]);var addr;switch(family){case 2:if(salen!==16){return{errno:28}}addr=HEAP32[sa+4>>2];addr=inetNtop4(addr);break;case 10:if(salen!==28){return{errno:28}}addr=[HEAP32[sa+8>>2],HEAP32[sa+12>>2],HEAP32[sa+16>>2],HEAP32[sa+20>>2]];addr=inetNtop6(addr);break;default:return{errno:5}}return{family:family,addr:addr,port:port}}function getSocketAddress(addrp,addrlen,allowNull){if(allowNull&&addrp===0)return null;var info=readSockaddr(addrp,addrlen);if(info.errno)throw new FS.ErrnoError(info.errno);info.addr=DNS.lookup_addr(info.addr)||info.addr;return info}function ___syscall_bind(fd,addr,addrlen,d1,d2,d3){try{var sock=getSocketFromFD(fd);var info=getSocketAddress(addr,addrlen);sock.sock_ops.bind(sock,info.addr,info.port);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_connect(fd,addr,addrlen,d1,d2,d3){try{var sock=getSocketFromFD(fd);var info=getSocketAddress(addr,addrlen);sock.sock_ops.connect(sock,info.addr,info.port);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=SYSCALLS.get();if(arg<0){return-28}var newStream;newStream=FS.createStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=SYSCALLS.get();stream.flags|=arg;return 0}case 5:{var arg=SYSCALLS.get();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 6:case 7:return 0;case 16:case 8:return-28;case 9:setErrNo(28);return-1;default:{return-28}}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}function ___syscall_getdents64(fd,dirp,count){try{var stream=SYSCALLS.getStreamFromFD(fd);if(!stream.getdents){stream.getdents=FS.readdir(stream.path)}var struct_size=280;var pos=0;var off=FS.llseek(stream,0,1);var idx=Math.floor(off/struct_size);while(idx>3]=BigInt(id);HEAP64[dirp+pos+8>>3]=BigInt((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;HEAP8[dirp+pos+18>>0]=type;stringToUTF8(name,dirp+pos+19,256);pos+=struct_size;idx+=1}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_getpeername(fd,addr,addrlen,d1,d2,d3){try{var sock=getSocketFromFD(fd);if(!sock.daddr){return-53}var errno=writeSockaddr(addr,sock.family,DNS.lookup_name(sock.daddr),sock.dport,addrlen);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_getsockname(fd,addr,addrlen,d1,d2,d3){try{var sock=getSocketFromFD(fd);var errno=writeSockaddr(addr,sock.family,DNS.lookup_name(sock.saddr||"0.0.0.0"),sock.sport,addrlen);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_getsockopt(fd,level,optname,optval,optlen,d1){try{var sock=getSocketFromFD(fd);if(level===1){if(optname===4){HEAP32[optval>>2]=sock.error;HEAP32[optlen>>2]=4;sock.error=null;return 0}}return-50}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:case 21505:{if(!stream.tty)return-59;return 0}case 21510:case 21511:case 21512:case 21506:case 21507:case 21508:{if(!stream.tty)return-59;return 0}case 21519:{if(!stream.tty)return-59;var argp=SYSCALLS.get();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=SYSCALLS.get();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;return 0}case 21524:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_listen(fd,backlog){try{var sock=getSocketFromFD(fd);sock.sock_ops.listen(sock,backlog);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.lstat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);path=PATH.normalize(path);if(path[path.length-1]==="/")path=path.substr(0,path.length-1);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.doStat(nofollow?FS.lstat:FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?SYSCALLS.get():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_poll(fds,nfds,timeout){try{var nonzero=0;for(var i=0;i>2];var events=HEAP16[pollfd+4>>1];var mask=32;var stream=FS.getStream(fd);if(stream){mask=SYSCALLS.DEFAULT_POLLMASK;if(stream.stream_ops.poll){mask=stream.stream_ops.poll(stream)}}mask&=events|8|16;if(mask)nonzero++;HEAP16[pollfd+6>>1]=mask}return nonzero}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_recvfrom(fd,buf,len,flags,addr,addrlen){try{var sock=getSocketFromFD(fd);var msg=sock.sock_ops.recvmsg(sock,len);if(!msg)return 0;if(addr){var errno=writeSockaddr(addr,sock.family,DNS.lookup_name(msg.addr),msg.port,addrlen)}HEAPU8.set(msg.buffer,buf);return msg.buffer.byteLength}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_renameat(olddirfd,oldpath,newdirfd,newpath){try{oldpath=SYSCALLS.getStr(oldpath);newpath=SYSCALLS.getStr(newpath);oldpath=SYSCALLS.calculateAt(olddirfd,oldpath);newpath=SYSCALLS.calculateAt(newdirfd,newpath);FS.rename(oldpath,newpath);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_sendto(fd,message,length,flags,addr,addr_len){try{var sock=getSocketFromFD(fd);var dest=getSocketAddress(addr,addr_len,true);if(!dest){return FS.write(sock.stream,HEAP8,message,length)}return sock.sock_ops.sendmsg(sock,HEAP8,message,length,dest.addr,dest.port)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_socket(domain,type,protocol){try{var sock=SOCKFS.createSocket(domain,type,protocol);return sock.stream.fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var nowIsMonotonic=true;function __emscripten_get_now_is_monotonic(){return nowIsMonotonic}function __emscripten_throw_longjmp(){throw Infinity}function readI53FromI64(ptr){return HEAPU32[ptr>>2]+HEAP32[ptr+4>>2]*4294967296}function __gmtime_js(time,tmPtr){var date=new Date(readI53FromI64(time)*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function isLeapYear(year){return year%4===0&&(year%100!==0||year%400===0)}var MONTH_DAYS_LEAP_CUMULATIVE=[0,31,60,91,121,152,182,213,244,274,305,335];var MONTH_DAYS_REGULAR_CUMULATIVE=[0,31,59,90,120,151,181,212,243,273,304,334];function ydayFromDate(date){var leap=isLeapYear(date.getFullYear());var monthDaysCumulative=leap?MONTH_DAYS_LEAP_CUMULATIVE:MONTH_DAYS_REGULAR_CUMULATIVE;var yday=monthDaysCumulative[date.getMonth()]+date.getDate()-1;return yday}function __localtime_js(time,tmPtr){var date=new Date(readI53FromI64(time)*1e3);HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr+36>>2]=-(date.getTimezoneOffset()*60);var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dst=(summerOffset!=winterOffset&&date.getTimezoneOffset()==Math.min(winterOffset,summerOffset))|0;HEAP32[tmPtr+32>>2]=dst}function __mktime_js(tmPtr){var date=new Date(HEAP32[tmPtr+20>>2]+1900,HEAP32[tmPtr+16>>2],HEAP32[tmPtr+12>>2],HEAP32[tmPtr+8>>2],HEAP32[tmPtr+4>>2],HEAP32[tmPtr>>2],0);var dst=HEAP32[tmPtr+32>>2];var guessedOffset=date.getTimezoneOffset();var start=new Date(date.getFullYear(),0,1);var summerOffset=new Date(date.getFullYear(),6,1).getTimezoneOffset();var winterOffset=start.getTimezoneOffset();var dstOffset=Math.min(winterOffset,summerOffset);if(dst<0){HEAP32[tmPtr+32>>2]=Number(summerOffset!=winterOffset&&dstOffset==guessedOffset)}else if(dst>0!=(dstOffset==guessedOffset)){var nonDstOffset=Math.max(winterOffset,summerOffset);var trueOffset=dst>0?dstOffset:nonDstOffset;date.setTime(date.getTime()+(trueOffset-guessedOffset)*6e4)}HEAP32[tmPtr+24>>2]=date.getDay();var yday=ydayFromDate(date)|0;HEAP32[tmPtr+28>>2]=yday;HEAP32[tmPtr>>2]=date.getSeconds();HEAP32[tmPtr+4>>2]=date.getMinutes();HEAP32[tmPtr+8>>2]=date.getHours();HEAP32[tmPtr+12>>2]=date.getDate();HEAP32[tmPtr+16>>2]=date.getMonth();HEAP32[tmPtr+20>>2]=date.getYear();return date.getTime()/1e3|0}function __mmap_js(len,prot,flags,fd,off,allocated,addr){try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,off,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}FS.munmap(stream)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function stringToNewUTF8(str){var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret}function __tzset_js(timezone,daylight,tzname){var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);function extractZone(date){var match=date.toTimeString().match(/\(([A-Za-z ]+)\)$/);return match?match[1]:"GMT"}var winterName=extractZone(winter);var summerName=extractZone(summer);var winterNamePtr=stringToNewUTF8(winterName);var summerNamePtr=stringToNewUTF8(summerName);if(summerOffset>2]=winterNamePtr;HEAPU32[tzname+4>>2]=summerNamePtr}else{HEAPU32[tzname>>2]=summerNamePtr;HEAPU32[tzname+4>>2]=winterNamePtr}}function _abort(){abort("")}Module["_abort"]=_abort;function _dlopen(handle){abort(dlopenMissingError)}var readEmAsmArgsArray=[];function readEmAsmArgs(sigPtr,buf){readEmAsmArgsArray.length=0;var ch;buf>>=2;while(ch=HEAPU8[sigPtr++]){buf+=ch!=105&buf;readEmAsmArgsArray.push(ch==105?HEAP32[buf]:(ch==106?HEAP64:HEAPF64)[buf++>>1]);++buf}return readEmAsmArgsArray}function runEmAsmFunction(code,sigPtr,argbuf){var args=readEmAsmArgs(sigPtr,argbuf);return ASM_CONSTS[code].apply(null,args)}function _emscripten_asm_const_int(code,sigPtr,argbuf){return runEmAsmFunction(code,sigPtr,argbuf)}function _emscripten_date_now(){return Date.now()}function getHeapMax(){return 2147483648}function _emscripten_get_heap_max(){return getHeapMax()}var _emscripten_get_now;if(ENVIRONMENT_IS_NODE){global.performance=require("perf_hooks").performance}_emscripten_get_now=()=>performance.now();function _emscripten_memcpy_big(dest,src,num){HEAPU8.copyWithin(dest,src,src+num)}function emscripten_realloc_buffer(size){var b=wasmMemory.buffer;try{wasmMemory.grow(size-b.byteLength+65535>>>16);updateMemoryViews();return 1}catch(e){}}function _emscripten_resize_heap(requestedSize){var oldSize=HEAPU8.length;requestedSize=requestedSize>>>0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}var alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=emscripten_realloc_buffer(newSize);if(replacement){return true}}return false}var ENV={};function getExecutableName(){return thisProgram||"./this.program"}function getEnvStrings(){if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={"USER":"web_user","LOGNAME":"web_user","PATH":"/","PWD":"/","HOME":"/home/web_user","LANG":lang,"_":getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings}function stringToAscii(str,buffer){for(var i=0;i>0]=str.charCodeAt(i)}HEAP8[buffer>>0]=0}function _environ_get(__environ,environ_buf){var bufSize=0;getEnvStrings().forEach(function(string,i){var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;stringToAscii(string,ptr);bufSize+=string.length+1});return 0}function _environ_sizes_get(penviron_count,penviron_buf_size){var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;strings.forEach(function(string){bufSize+=string.length+1});HEAPU32[penviron_buf_size>>2]=bufSize;return 0}function _proc_exit(code){EXITSTATUS=code;if(!keepRuntimeAlive()){if(Module["onExit"])Module["onExit"](code);ABORT=true}quit_(code,new ExitStatus(code))}function exitJS(status,implicit){EXITSTATUS=status;_proc_exit(status)}var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_fdstat_get(fd,pbuf){try{var rightsBase=0;var rightsInheriting=0;var flags=0;{var stream=SYSCALLS.getStreamFromFD(fd);var type=stream.tty?2:FS.isDir(stream.mode)?3:FS.isLink(stream.mode)?7:4}HEAP8[pbuf>>0]=type;HEAP16[pbuf+2>>1]=flags;HEAP64[pbuf+8>>3]=BigInt(rightsBase);HEAP64[pbuf+16>>3]=BigInt(rightsInheriting);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function doReadv(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var MAX_INT53=9007199254740992;var MIN_INT53=-9007199254740992;function bigintToI53Checked(num){return numMAX_INT53?NaN:Number(num)}function _fd_seek(fd,offset,whence,newOffset){try{offset=bigintToI53Checked(offset);if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function doWritev(stream,iov,iovcnt,offset){var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(typeof offset!=="undefined"){offset+=curr}}return ret}function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _getaddrinfo(node,service,hint,out){var addr=0;var port=0;var flags=0;var family=0;var type=0;var proto=0;var ai;function allocaddrinfo(family,type,proto,canon,addr,port){var sa,salen,ai;var errno;salen=family===10?28:16;addr=family===10?inetNtop6(addr):inetNtop4(addr);sa=_malloc(salen);errno=writeSockaddr(sa,family,addr,port);assert(!errno);ai=_malloc(32);HEAP32[ai+4>>2]=family;HEAP32[ai+8>>2]=type;HEAP32[ai+12>>2]=proto;HEAPU32[ai+24>>2]=canon;HEAPU32[ai+20>>2]=sa;if(family===10){HEAP32[ai+16>>2]=28}else{HEAP32[ai+16>>2]=16}HEAP32[ai+28>>2]=0;return ai}if(hint){flags=HEAP32[hint>>2];family=HEAP32[hint+4>>2];type=HEAP32[hint+8>>2];proto=HEAP32[hint+12>>2]}if(type&&!proto){proto=type===2?17:6}if(!type&&proto){type=proto===17?2:1}if(proto===0){proto=6}if(type===0){type=1}if(!node&&!service){return-2}if(flags&~(1|2|4|1024|8|16|32)){return-1}if(hint!==0&&HEAP32[hint>>2]&2&&!node){return-1}if(flags&32){return-2}if(type!==0&&type!==1&&type!==2){return-7}if(family!==0&&family!==2&&family!==10){return-6}if(service){service=UTF8ToString(service);port=parseInt(service,10);if(isNaN(port)){if(flags&1024){return-2}return-8}}if(!node){if(family===0){family=2}if((flags&1)===0){if(family===2){addr=_htonl(2130706433)}else{addr=[0,0,0,1]}}ai=allocaddrinfo(family,type,proto,null,addr,port);HEAPU32[out>>2]=ai;return 0}node=UTF8ToString(node);addr=inetPton4(node);if(addr!==null){if(family===0||family===2){family=2}else if(family===10&&flags&8){addr=[0,0,_htonl(65535),addr];family=10}else{return-2}}else{addr=inetPton6(node);if(addr!==null){if(family===0||family===10){family=10}else{return-2}}}if(addr!=null){ai=allocaddrinfo(family,type,proto,node,addr,port);HEAPU32[out>>2]=ai;return 0}if(flags&4){return-2}node=DNS.lookup_name(node);addr=inetPton4(node);if(family===0){family=2}else if(family===10){addr=[0,0,_htonl(65535),addr]}ai=allocaddrinfo(family,type,proto,null,addr,port);HEAPU32[out>>2]=ai;return 0}function _getnameinfo(sa,salen,node,nodelen,serv,servlen,flags){var info=readSockaddr(sa,salen);if(info.errno){return-6}var port=info.port;var addr=info.addr;var overflowed=false;if(node&&nodelen){var lookup;if(flags&1||!(lookup=DNS.lookup_addr(addr))){if(flags&8){return-2}}else{addr=lookup}var numBytesWrittenExclNull=stringToUTF8(addr,node,nodelen);if(numBytesWrittenExclNull+1>=nodelen){overflowed=true}}if(serv&&servlen){port=""+port;var numBytesWrittenExclNull=stringToUTF8(port,serv,servlen);if(numBytesWrittenExclNull+1>=servlen){overflowed=true}}if(overflowed){return-12}return 0}function arraySum(array,index){var sum=0;for(var i=0;i<=index;sum+=array[i++]){}return sum}var MONTH_DAYS_LEAP=[31,29,31,30,31,30,31,31,30,31,30,31];var MONTH_DAYS_REGULAR=[31,28,31,30,31,30,31,31,30,31,30,31];function addDays(date,days){var newDate=new Date(date.getTime());while(days>0){var leap=isLeapYear(newDate.getFullYear());var currentMonth=newDate.getMonth();var daysInCurrentMonth=(leap?MONTH_DAYS_LEAP:MONTH_DAYS_REGULAR)[currentMonth];if(days>daysInCurrentMonth-newDate.getDate()){days-=daysInCurrentMonth-newDate.getDate()+1;newDate.setDate(1);if(currentMonth<11){newDate.setMonth(currentMonth+1)}else{newDate.setMonth(0);newDate.setFullYear(newDate.getFullYear()+1)}}else{newDate.setDate(newDate.getDate()+days);return newDate}}return newDate}function writeArrayToMemory(array,buffer){HEAP8.set(array,buffer)}function _strftime(s,maxsize,format,tm){var tm_zone=HEAP32[tm+40>>2];var date={tm_sec:HEAP32[tm>>2],tm_min:HEAP32[tm+4>>2],tm_hour:HEAP32[tm+8>>2],tm_mday:HEAP32[tm+12>>2],tm_mon:HEAP32[tm+16>>2],tm_year:HEAP32[tm+20>>2],tm_wday:HEAP32[tm+24>>2],tm_yday:HEAP32[tm+28>>2],tm_isdst:HEAP32[tm+32>>2],tm_gmtoff:HEAP32[tm+36>>2],tm_zone:tm_zone?UTF8ToString(tm_zone):""};var pattern=UTF8ToString(format);var EXPANSION_RULES_1={"%c":"%a %b %d %H:%M:%S %Y","%D":"%m/%d/%y","%F":"%Y-%m-%d","%h":"%b","%r":"%I:%M:%S %p","%R":"%H:%M","%T":"%H:%M:%S","%x":"%m/%d/%y","%X":"%H:%M:%S","%Ec":"%c","%EC":"%C","%Ex":"%m/%d/%y","%EX":"%H:%M:%S","%Ey":"%y","%EY":"%Y","%Od":"%d","%Oe":"%e","%OH":"%H","%OI":"%I","%Om":"%m","%OM":"%M","%OS":"%S","%Ou":"%u","%OU":"%U","%OV":"%V","%Ow":"%w","%OW":"%W","%Oy":"%y"};for(var rule in EXPANSION_RULES_1){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_1[rule])}var WEEKDAYS=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];var MONTHS=["January","February","March","April","May","June","July","August","September","October","November","December"];function leadingSomething(value,digits,character){var str=typeof value=="number"?value.toString():value||"";while(str.length0?1:0}var compare;if((compare=sgn(date1.getFullYear()-date2.getFullYear()))===0){if((compare=sgn(date1.getMonth()-date2.getMonth()))===0){compare=sgn(date1.getDate()-date2.getDate())}}return compare}function getFirstWeekStartDate(janFourth){switch(janFourth.getDay()){case 0:return new Date(janFourth.getFullYear()-1,11,29);case 1:return janFourth;case 2:return new Date(janFourth.getFullYear(),0,3);case 3:return new Date(janFourth.getFullYear(),0,2);case 4:return new Date(janFourth.getFullYear(),0,1);case 5:return new Date(janFourth.getFullYear()-1,11,31);case 6:return new Date(janFourth.getFullYear()-1,11,30)}}function getWeekBasedYear(date){var thisDate=addDays(new Date(date.tm_year+1900,0,1),date.tm_yday);var janFourthThisYear=new Date(thisDate.getFullYear(),0,4);var janFourthNextYear=new Date(thisDate.getFullYear()+1,0,4);var firstWeekStartThisYear=getFirstWeekStartDate(janFourthThisYear);var firstWeekStartNextYear=getFirstWeekStartDate(janFourthNextYear);if(compareByDay(firstWeekStartThisYear,thisDate)<=0){if(compareByDay(firstWeekStartNextYear,thisDate)<=0){return thisDate.getFullYear()+1}return thisDate.getFullYear()}return thisDate.getFullYear()-1}var EXPANSION_RULES_2={"%a":function(date){return WEEKDAYS[date.tm_wday].substring(0,3)},"%A":function(date){return WEEKDAYS[date.tm_wday]},"%b":function(date){return MONTHS[date.tm_mon].substring(0,3)},"%B":function(date){return MONTHS[date.tm_mon]},"%C":function(date){var year=date.tm_year+1900;return leadingNulls(year/100|0,2)},"%d":function(date){return leadingNulls(date.tm_mday,2)},"%e":function(date){return leadingSomething(date.tm_mday,2," ")},"%g":function(date){return getWeekBasedYear(date).toString().substring(2)},"%G":function(date){return getWeekBasedYear(date)},"%H":function(date){return leadingNulls(date.tm_hour,2)},"%I":function(date){var twelveHour=date.tm_hour;if(twelveHour==0)twelveHour=12;else if(twelveHour>12)twelveHour-=12;return leadingNulls(twelveHour,2)},"%j":function(date){return leadingNulls(date.tm_mday+arraySum(isLeapYear(date.tm_year+1900)?MONTH_DAYS_LEAP:MONTH_DAYS_REGULAR,date.tm_mon-1),3)},"%m":function(date){return leadingNulls(date.tm_mon+1,2)},"%M":function(date){return leadingNulls(date.tm_min,2)},"%n":function(){return"\n"},"%p":function(date){if(date.tm_hour>=0&&date.tm_hour<12){return"AM"}return"PM"},"%S":function(date){return leadingNulls(date.tm_sec,2)},"%t":function(){return"\t"},"%u":function(date){return date.tm_wday||7},"%U":function(date){var days=date.tm_yday+7-date.tm_wday;return leadingNulls(Math.floor(days/7),2)},"%V":function(date){var val=Math.floor((date.tm_yday+7-(date.tm_wday+6)%7)/7);if((date.tm_wday+371-date.tm_yday-2)%7<=2){val++}if(!val){val=52;var dec31=(date.tm_wday+7-date.tm_yday-1)%7;if(dec31==4||dec31==5&&isLeapYear(date.tm_year%400-1)){val++}}else if(val==53){var jan1=(date.tm_wday+371-date.tm_yday)%7;if(jan1!=4&&(jan1!=3||!isLeapYear(date.tm_year)))val=1}return leadingNulls(val,2)},"%w":function(date){return date.tm_wday},"%W":function(date){var days=date.tm_yday+7-(date.tm_wday+6)%7;return leadingNulls(Math.floor(days/7),2)},"%y":function(date){return(date.tm_year+1900).toString().substring(2)},"%Y":function(date){return date.tm_year+1900},"%z":function(date){var off=date.tm_gmtoff;var ahead=off>=0;off=Math.abs(off)/60;off=off/60*100+off%60;return(ahead?"+":"-")+String("0000"+off).slice(-4)},"%Z":function(date){return date.tm_zone},"%%":function(){return"%"}};pattern=pattern.replace(/%%/g,"\0\0");for(var rule in EXPANSION_RULES_2){if(pattern.includes(rule)){pattern=pattern.replace(new RegExp(rule,"g"),EXPANSION_RULES_2[rule](date))}}pattern=pattern.replace(/\0\0/g,"%");var bytes=intArrayFromString(pattern,false);if(bytes.length>maxsize){return 0}writeArrayToMemory(bytes,s);return bytes.length-1}var FSNode=function(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev};var readMode=292|73;var writeMode=146;Object.defineProperties(FSNode.prototype,{read:{get:function(){return(this.mode&readMode)===readMode},set:function(val){val?this.mode|=readMode:this.mode&=~readMode}},write:{get:function(){return(this.mode&writeMode)===writeMode},set:function(val){val?this.mode|=writeMode:this.mode&=~writeMode}},isFolder:{get:function(){return FS.isDir(this.mode)}},isDevice:{get:function(){return FS.isChrdev(this.mode)}}});FS.FSNode=FSNode;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();var wasmImports={"b":___assert_fail,"f":___cxa_throw,"ka":___dlsym,"R":___syscall__newselect,"L":___syscall_accept4,"K":___syscall_bind,"J":___syscall_connect,"la":___syscall_faccessat,"g":___syscall_fcntl64,"ha":___syscall_fstat64,"U":___syscall_getdents64,"I":___syscall_getpeername,"H":___syscall_getsockname,"G":___syscall_getsockopt,"y":___syscall_ioctl,"F":___syscall_listen,"ea":___syscall_lstat64,"$":___syscall_mkdirat,"fa":___syscall_newfstatat,"w":___syscall_openat,"V":___syscall_poll,"E":___syscall_recvfrom,"T":___syscall_renameat,"S":___syscall_rmdir,"D":___syscall_sendto,"v":___syscall_socket,"ga":___syscall_stat64,"O":___syscall_unlinkat,"ia":__emscripten_get_now_is_monotonic,"M":__emscripten_throw_longjmp,"Y":__gmtime_js,"Z":__localtime_js,"_":__mktime_js,"W":__mmap_js,"X":__munmap_js,"P":__tzset_js,"a":_abort,"t":_dlopen,"oa":_emscripten_asm_const_int,"l":_emscripten_date_now,"Q":_emscripten_get_heap_max,"p":_emscripten_get_now,"ja":_emscripten_memcpy_big,"N":_emscripten_resize_heap,"ca":_environ_get,"da":_environ_sizes_get,"o":_exit,"m":_fd_close,"ba":_fd_fdstat_get,"x":_fd_read,"aa":_fd_seek,"q":_fd_write,"k":_getaddrinfo,"i":_getnameinfo,"pa":invoke_i,"na":invoke_ii,"c":invoke_iii,"n":invoke_iiii,"s":invoke_iiiii,"z":invoke_iiiiii,"r":invoke_iiiiiiiii,"B":invoke_iiiijj,"qa":invoke_iij,"h":invoke_vi,"j":invoke_vii,"d":invoke_viiii,"ma":invoke_viiiiii,"A":invoke_viiiiiiii,"C":is_timeout,"u":send_progress,"e":_strftime};var asm=createWasm();var ___wasm_call_ctors=function(){return(___wasm_call_ctors=Module["asm"]["sa"]).apply(null,arguments)};var _malloc=Module["_malloc"]=function(){return(_malloc=Module["_malloc"]=Module["asm"]["ta"]).apply(null,arguments)};var ___errno_location=function(){return(___errno_location=Module["asm"]["va"]).apply(null,arguments)};var _ntohs=function(){return(_ntohs=Module["asm"]["wa"]).apply(null,arguments)};var _htons=function(){return(_htons=Module["asm"]["xa"]).apply(null,arguments)};var _ffmpeg=Module["_ffmpeg"]=function(){return(_ffmpeg=Module["_ffmpeg"]=Module["asm"]["ya"]).apply(null,arguments)};var _htonl=function(){return(_htonl=Module["asm"]["za"]).apply(null,arguments)};var _emscripten_builtin_memalign=function(){return(_emscripten_builtin_memalign=Module["asm"]["Aa"]).apply(null,arguments)};var _setThrew=function(){return(_setThrew=Module["asm"]["Ba"]).apply(null,arguments)};var stackSave=function(){return(stackSave=Module["asm"]["Ca"]).apply(null,arguments)};var stackRestore=function(){return(stackRestore=Module["asm"]["Da"]).apply(null,arguments)};var ___cxa_is_pointer_type=function(){return(___cxa_is_pointer_type=Module["asm"]["Ea"]).apply(null,arguments)};var _ff_h264_cabac_tables=Module["_ff_h264_cabac_tables"]=1537004;var ___start_em_js=Module["___start_em_js"]=6059629;var ___stop_em_js=Module["___stop_em_js"]=6059806;function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}Module["setValue"]=setValue;Module["getValue"]=getValue;Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["FS"]=FS;var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run(); + + + return createFFmpegCore.ready +} + +); +})(); +if (typeof exports === 'object' && typeof module === 'object') + module.exports = createFFmpegCore; +else if (typeof define === 'function' && define['amd']) + define([], function() { return createFFmpegCore; }); +else if (typeof exports === 'object') + exports["createFFmpegCore"] = createFFmpegCore; diff --git a/app/static/vendor/ffmpeg/ffmpeg-core.wasm b/app/static/vendor/ffmpeg/ffmpeg-core.wasm new file mode 100644 index 0000000..8988d43 Binary files /dev/null and b/app/static/vendor/ffmpeg/ffmpeg-core.wasm differ diff --git a/app/static/vendor/ffmpeg/ffmpeg-util.js b/app/static/vendor/ffmpeg/ffmpeg-util.js new file mode 100644 index 0000000..e3fae0b --- /dev/null +++ b/app/static/vendor/ffmpeg/ffmpeg-util.js @@ -0,0 +1 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegUtil=t():e.FFmpegUtil=t()}(self,(()=>(()=>{"use strict";var e={591:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.HeaderContentLength=void 0,t.HeaderContentLength="Content-Length"},431:(e,t)=>{Object.defineProperty(t,"__esModule",{value:!0}),t.ERROR_INCOMPLETED_DOWNLOAD=t.ERROR_RESPONSE_BODY_READER=void 0,t.ERROR_RESPONSE_BODY_READER=new Error("failed to get response body reader"),t.ERROR_INCOMPLETED_DOWNLOAD=new Error("failed to complete download")},915:function(e,t,o){var r=this&&this.__awaiter||function(e,t,o,r){return new(o||(o=Promise))((function(n,i){function d(e){try{l(r.next(e))}catch(e){i(e)}}function a(e){try{l(r.throw(e))}catch(e){i(e)}}function l(e){var t;e.done?n(e.value):(t=e.value,t instanceof o?t:new o((function(e){e(t)}))).then(d,a)}l((r=r.apply(e,t||[])).next())}))};Object.defineProperty(t,"__esModule",{value:!0}),t.toBlobURL=t.downloadWithProgress=t.importScript=t.fetchFile=void 0;const n=o(431),i=o(591);t.fetchFile=e=>r(void 0,void 0,void 0,(function*(){let t;if("string"==typeof e)t=/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(e)?atob(e.split(",")[1]).split("").map((e=>e.charCodeAt(0))):yield(yield fetch(e)).arrayBuffer();else if(e instanceof URL)t=yield(yield fetch(e)).arrayBuffer();else{if(!(e instanceof File||e instanceof Blob))return new Uint8Array;t=yield(o=e,new Promise(((e,t)=>{const r=new FileReader;r.onload=()=>{const{result:t}=r;t instanceof ArrayBuffer?e(new Uint8Array(t)):e(new Uint8Array)},r.onerror=e=>{var o,r;t(Error(`File could not be read! Code=${(null===(r=null===(o=null==e?void 0:e.target)||void 0===o?void 0:o.error)||void 0===r?void 0:r.code)||-1}`))},r.readAsArrayBuffer(o)})))}var o;return new Uint8Array(t)})),t.importScript=e=>r(void 0,void 0,void 0,(function*(){return new Promise((t=>{const o=document.createElement("script"),r=()=>{o.removeEventListener("load",r),t()};o.src=e,o.type="text/javascript",o.addEventListener("load",r),document.getElementsByTagName("head")[0].appendChild(o)}))})),t.downloadWithProgress=(e,t)=>r(void 0,void 0,void 0,(function*(){var o;const r=yield fetch(e);let d;try{const a=parseInt(r.headers.get(i.HeaderContentLength)||"-1"),l=null===(o=r.body)||void 0===o?void 0:o.getReader();if(!l)throw n.ERROR_RESPONSE_BODY_READER;const c=[];let s=0;for(;;){const{done:o,value:r}=yield l.read(),i=r?r.length:0;if(o){if(-1!=a&&a!==s)throw n.ERROR_INCOMPLETED_DOWNLOAD;t&&t({url:e,total:a,received:s,delta:i,done:o});break}c.push(r),s+=i,t&&t({url:e,total:a,received:s,delta:i,done:o})}const f=new Uint8Array(s);let u=0;for(const e of c)f.set(e,u),u+=e.length;d=f.buffer}catch(o){console.log("failed to send download progress event: ",o),d=yield r.arrayBuffer(),t&&t({url:e,total:d.byteLength,received:d.byteLength,delta:0,done:!0})}return d})),t.toBlobURL=(e,o,n=!1,i)=>r(void 0,void 0,void 0,(function*(){const r=n?yield(0,t.downloadWithProgress)(e,i):yield(yield fetch(e)).arrayBuffer(),d=new Blob([r],{type:o});return URL.createObjectURL(d)}))}},t={};return function o(r){var n=t[r];if(void 0!==n)return n.exports;var i=t[r]={exports:{}};return e[r].call(i.exports,i,i.exports,o),i.exports}(915)})())); \ No newline at end of file diff --git a/app/static/vendor/ffmpeg/ffmpeg.js b/app/static/vendor/ffmpeg/ffmpeg.js new file mode 100644 index 0000000..05cdf7a --- /dev/null +++ b/app/static/vendor/ffmpeg/ffmpeg.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{"use strict";var e={m:{},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},u:e=>e+".ffmpeg.js"};e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var s=e.g.document;if(!t&&s&&(s.currentScript&&(t=s.currentScript.src),!t)){var r=s.getElementsByTagName("script");if(r.length)for(var a=r.length-1;a>-1&&!t;)t=r[a--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.b=document.baseURI||self.location.href;var t,s={};e.r(s),e.d(s,{FFmpeg:()=>i}),function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const r=(()=>{let e=0;return()=>e++})(),a=(new Error("unknown message type"),new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first")),o=new Error("called FFmpeg.terminate()");new Error("failed to import ffmpeg-core.js");class i{#e=null;#t={};#s={};#r=[];#a=[];loaded=!1;#o=()=>{this.#e&&(this.#e.onmessage=({data:{id:e,type:s,data:r}})=>{switch(s){case t.LOAD:this.loaded=!0,this.#t[e](r);break;case t.MOUNT:case t.UNMOUNT:case t.EXEC:case t.WRITE_FILE:case t.READ_FILE:case t.DELETE_FILE:case t.RENAME:case t.CREATE_DIR:case t.LIST_DIR:case t.DELETE_DIR:this.#t[e](r);break;case t.LOG:this.#r.forEach((e=>e(r)));break;case t.PROGRESS:this.#a.forEach((e=>e(r)));break;case t.ERROR:this.#s[e](r)}delete this.#t[e],delete this.#s[e]})};#i=({type:e,data:t},s=[],o)=>this.#e?new Promise(((a,i)=>{const n=r();this.#e&&this.#e.postMessage({id:n,type:e,data:t},s),this.#t[n]=a,this.#s[n]=i,o?.addEventListener("abort",(()=>{i(new DOMException(`Message # ${n} was aborted`,"AbortError"))}),{once:!0})})):Promise.reject(a);on(e,t){"log"===e?this.#r.push(t):"progress"===e&&this.#a.push(t)}off(e,t){"log"===e?this.#r=this.#r.filter((e=>e!==t)):"progress"===e&&(this.#a=this.#a.filter((e=>e!==t)))}load=({classWorkerURL:s,...r}={},{signal:a}={})=>(this.#e||(this.#e=s?new Worker(new URL(s,"file:///home/jeromewu/ffmpeg.wasm/packages/ffmpeg/dist/esm/classes.js"),{type:"module"}):new Worker(new URL(e.p+e.u(814),e.b),{type:void 0}),this.#o()),this.#i({type:t.LOAD,data:r},void 0,a));exec=(e,s=-1,{signal:r}={})=>this.#i({type:t.EXEC,data:{args:e,timeout:s}},void 0,r);terminate=()=>{const e=Object.keys(this.#s);for(const t of e)this.#s[t](o),delete this.#s[t],delete this.#t[t];this.#e&&(this.#e.terminate(),this.#e=null,this.loaded=!1)};writeFile=(e,s,{signal:r}={})=>{const a=[];return s instanceof Uint8Array&&a.push(s.buffer),this.#i({type:t.WRITE_FILE,data:{path:e,data:s}},a,r)};mount=(e,s,r)=>this.#i({type:t.MOUNT,data:{fsType:e,options:s,mountPoint:r}},[]);unmount=e=>this.#i({type:t.UNMOUNT,data:{mountPoint:e}},[]);readFile=(e,s="binary",{signal:r}={})=>this.#i({type:t.READ_FILE,data:{path:e,encoding:s}},void 0,r);deleteFile=(e,{signal:s}={})=>this.#i({type:t.DELETE_FILE,data:{path:e}},void 0,s);rename=(e,s,{signal:r}={})=>this.#i({type:t.RENAME,data:{oldPath:e,newPath:s}},void 0,r);createDir=(e,{signal:s}={})=>this.#i({type:t.CREATE_DIR,data:{path:e}},void 0,s);listDir=(e,{signal:s}={})=>this.#i({type:t.LIST_DIR,data:{path:e}},void 0,s);deleteDir=(e,{signal:s}={})=>this.#i({type:t.DELETE_DIR,data:{path:e}},void 0,s)}return s})())); +//# sourceMappingURL=ffmpeg.js.map \ No newline at end of file diff --git a/app/static/vendor/ffmpeg/ffmpeg.js.map b/app/static/vendor/ffmpeg/ffmpeg.js.map new file mode 100644 index 0000000..d328f05 --- /dev/null +++ b/app/static/vendor/ffmpeg/ffmpeg.js.map @@ -0,0 +1 @@ +{"version":3,"file":"ffmpeg.js","mappings":"CAAA,SAA2CA,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAoB,WAAID,IAExBD,EAAiB,WAAIC,GACtB,CATD,CASGK,MAAM,I,uBCRLC,EAAsB,CAG1BA,E,GCHAA,EAAwB,CAACL,EAASM,KACjC,IAAI,IAAIC,KAAOD,EACXD,EAAoBG,EAAEF,EAAYC,KAASF,EAAoBG,EAAER,EAASO,IAC5EE,OAAOC,eAAeV,EAASO,EAAK,CAAEI,YAAY,EAAMC,IAAKN,EAAWC,IAE1E,ECLDF,EAAyBQ,GAEZA,EAAU,cCHvBR,EAAoBS,EAAI,WACvB,GAA0B,iBAAfC,WAAyB,OAAOA,WAC3C,IACC,OAAOC,MAAQ,IAAIC,SAAS,cAAb,EAChB,CAAE,MAAOC,GACR,GAAsB,iBAAXC,OAAqB,OAAOA,MACxC,CACA,CAPuB,GCAxBd,EAAoBG,EAAI,CAACY,EAAKC,IAAUZ,OAAOa,UAAUC,eAAeC,KAAKJ,EAAKC,GCClFhB,EAAoBoB,EAAKzB,IACH,oBAAX0B,QAA0BA,OAAOC,aAC1ClB,OAAOC,eAAeV,EAAS0B,OAAOC,YAAa,CAAEC,MAAO,WAE7DnB,OAAOC,eAAeV,EAAS,aAAc,CAAE4B,OAAO,GAAO,E,MCL9D,IAAIC,EACAxB,EAAoBS,EAAEgB,gBAAeD,EAAYxB,EAAoBS,EAAEiB,SAAW,IACtF,IAAIC,EAAW3B,EAAoBS,EAAEkB,SACrC,IAAKH,GAAaG,IACbA,EAASC,gBACZJ,EAAYG,EAASC,cAAcC,MAC/BL,GAAW,CACf,IAAIM,EAAUH,EAASI,qBAAqB,UAC5C,GAAGD,EAAQE,OAEV,IADA,IAAIC,EAAIH,EAAQE,OAAS,EAClBC,GAAK,IAAMT,GAAWA,EAAYM,EAAQG,KAAKJ,GAExD,CAID,IAAKL,EAAW,MAAM,IAAIU,MAAM,yDAChCV,EAAYA,EAAUW,QAAQ,OAAQ,IAAIA,QAAQ,QAAS,IAAIA,QAAQ,YAAa,KACpFnC,EAAoBoC,EAAIZ,C,KClBxBxB,EAAoBqC,EAAIV,SAASW,SAAWvC,KAAK2B,SAASa,K,ICI/CC,E,kCACX,SAAWA,GACPA,EAAoB,KAAI,OACxBA,EAAoB,KAAI,OACxBA,EAA0B,WAAI,aAC9BA,EAAyB,UAAI,YAC7BA,EAA2B,YAAI,cAC/BA,EAAsB,OAAI,SAC1BA,EAA0B,WAAI,aAC9BA,EAAwB,SAAI,WAC5BA,EAA0B,WAAI,aAC9BA,EAAqB,MAAI,QACzBA,EAAwB,SAAI,WAC5BA,EAAwB,SAAI,WAC5BA,EAAmB,IAAI,MACvBA,EAAqB,MAAI,QACzBA,EAAuB,QAAI,SAC9B,CAhBD,CAgBGA,IAAkBA,EAAgB,CAAC,IClB/B,MAAMC,EAAe,MACxB,IAAIC,EAAY,EAChB,MAAO,IAAMA,GAChB,EAH2B,GCFfC,GAD6B,IAAIT,MAAM,wBACpB,IAAIA,MAAM,2DAC7BU,EAAmB,IAAIV,MAAM,6BACN,IAAIA,MAAM,mCCQvC,MAAMW,EACT,GAAU,KAKV,GAAY,CAAC,EACb,GAAW,CAAC,EACZ,GAAqB,GACrB,GAA0B,GAC1BC,QAAS,EAIT,GAAoB,KACZnC,MAAK,IACLA,MAAK,EAAQoC,UAAY,EAAGC,MAAQC,KAAIC,OAAMF,YAC1C,OAAQE,GACJ,KAAKV,EAAcW,KACfxC,KAAKmC,QAAS,EACdnC,MAAK,EAAUsC,GAAID,GACnB,MACJ,KAAKR,EAAcY,MACnB,KAAKZ,EAAca,QACnB,KAAKb,EAAcc,KACnB,KAAKd,EAAce,WACnB,KAAKf,EAAcgB,UACnB,KAAKhB,EAAciB,YACnB,KAAKjB,EAAckB,OACnB,KAAKlB,EAAcmB,WACnB,KAAKnB,EAAcoB,SACnB,KAAKpB,EAAcqB,WACflD,MAAK,EAAUsC,GAAID,GACnB,MACJ,KAAKR,EAAcsB,IACfnD,MAAK,EAAmBoD,SAASC,GAAMA,EAAEhB,KACzC,MACJ,KAAKR,EAAcyB,SACftD,MAAK,EAAwBoD,SAASC,GAAMA,EAAEhB,KAC9C,MACJ,KAAKR,EAAc0B,MACfvD,MAAK,EAASsC,GAAID,UAGnBrC,MAAK,EAAUsC,UACftC,MAAK,EAASsC,EAAG,EAEhC,EAKJ,GAAQ,EAAGC,OAAMF,QAAQmB,EAAQ,GAAIC,IAC5BzD,MAAK,EAGH,IAAI0D,SAAQ,CAACC,EAASC,KACzB,MAAMtB,EAAKR,IACX9B,MAAK,GAAWA,MAAK,EAAQ6D,YAAY,CAAEvB,KAAIC,OAAMF,QAAQmB,GAC7DxD,MAAK,EAAUsC,GAAMqB,EACrB3D,MAAK,EAASsC,GAAMsB,EACpBH,GAAQK,iBAAiB,SAAS,KAC9BF,EAAO,IAAIG,aAAa,aAAazB,gBAAkB,cAAc,GACtE,CAAE0B,MAAM,GAAO,IATXN,QAAQE,OAAO5B,GAY9B,EAAAiC,CAAGC,EAAOC,GACQ,QAAVD,EACAlE,MAAK,EAAmBoE,KAAKD,GAEd,aAAVD,GACLlE,MAAK,EAAwBoE,KAAKD,EAE1C,CACA,GAAAE,CAAIH,EAAOC,GACO,QAAVD,EACAlE,MAAK,EAAqBA,MAAK,EAAmBsE,QAAQjB,GAAMA,IAAMc,IAEvD,aAAVD,IACLlE,MAAK,EAA0BA,MAAK,EAAwBsE,QAAQjB,GAAMA,IAAMc,IAExF,CAQAI,KAAO,EAAGC,oBAAmBC,GAAW,CAAC,GAAKhB,UAAW,CAAC,KACjDzD,MAAK,IACNA,MAAK,EAAUwE,EACX,IAAIE,OAAO,IAAIC,IAAIH,EAAgB,yEAAkB,CACjDjC,KAAM,WAIV,IAAImC,OAAO,IAAIC,IAAI,kBAAiC,CAChDpC,UAAM,IAEdvC,MAAK,KAEFA,MAAK,EAAM,CACduC,KAAMV,EAAcW,KACpBH,KAAMoC,QACPG,EAAWnB,IAsBlBoB,KAAO,CAEPC,EAMAC,GAAU,GAAMtB,UAAW,CAAC,IAAMzD,MAAK,EAAM,CACzCuC,KAAMV,EAAcc,KACpBN,KAAM,CAAEyC,OAAMC,iBACfH,EAAWnB,GAOduB,UAAY,KACR,MAAMC,EAAMxF,OAAOyF,KAAKlF,MAAK,GAE7B,IAAK,MAAMsC,KAAM2C,EACbjF,MAAK,EAASsC,GAAIL,UACXjC,MAAK,EAASsC,UACdtC,MAAK,EAAUsC,GAEtBtC,MAAK,IACLA,MAAK,EAAQgF,YACbhF,MAAK,EAAU,KACfA,KAAKmC,QAAS,EAClB,EAeJgD,UAAY,CAACC,EAAM/C,GAAQoB,UAAW,CAAC,KACnC,MAAMD,EAAQ,GAId,OAHInB,aAAgBgD,YAChB7B,EAAMY,KAAK/B,EAAKiD,QAEbtF,MAAK,EAAM,CACduC,KAAMV,EAAce,WACpBP,KAAM,CAAE+C,OAAM/C,SACfmB,EAAOC,EAAO,EAErB8B,MAAQ,CAACC,EAAQC,EAASC,IAEf1F,MAAK,EAAM,CACduC,KAAMV,EAAcY,MACpBJ,KAAM,CAAEmD,SAAQC,UAASC,eAHf,IAMlBC,QAAWD,GAEA1F,MAAK,EAAM,CACduC,KAAMV,EAAca,QACpBL,KAAM,CAAEqD,eAHE,IAkBlBE,SAAW,CAACR,EAQZS,EAAW,UAAYpC,UAAW,CAAC,IAAMzD,MAAK,EAAM,CAChDuC,KAAMV,EAAcgB,UACpBR,KAAM,CAAE+C,OAAMS,kBACfjB,EAAWnB,GAMdqC,WAAa,CAACV,GAAQ3B,UAAW,CAAC,IAAMzD,MAAK,EAAM,CAC/CuC,KAAMV,EAAciB,YACpBT,KAAM,CAAE+C,cACTR,EAAWnB,GAMdsC,OAAS,CAACC,EAASC,GAAWxC,UAAW,CAAC,IAAMzD,MAAK,EAAM,CACvDuC,KAAMV,EAAckB,OACpBV,KAAM,CAAE2D,UAASC,iBAClBrB,EAAWnB,GAMdyC,UAAY,CAACd,GAAQ3B,UAAW,CAAC,IAAMzD,MAAK,EAAM,CAC9CuC,KAAMV,EAAcmB,WACpBX,KAAM,CAAE+C,cACTR,EAAWnB,GAMd0C,QAAU,CAACf,GAAQ3B,UAAW,CAAC,IAAMzD,MAAK,EAAM,CAC5CuC,KAAMV,EAAcoB,SACpBZ,KAAM,CAAE+C,cACTR,EAAWnB,GAMd2C,UAAY,CAAChB,GAAQ3B,UAAW,CAAC,IAAMzD,MAAK,EAAM,CAC9CuC,KAAMV,EAAcqB,WACpBb,KAAM,CAAE+C,cACTR,EAAWnB,G","sources":["webpack://FFmpegWASM/webpack/universalModuleDefinition","webpack://FFmpegWASM/webpack/bootstrap","webpack://FFmpegWASM/webpack/runtime/define property getters","webpack://FFmpegWASM/webpack/runtime/get javascript chunk filename","webpack://FFmpegWASM/webpack/runtime/global","webpack://FFmpegWASM/webpack/runtime/hasOwnProperty shorthand","webpack://FFmpegWASM/webpack/runtime/make namespace object","webpack://FFmpegWASM/webpack/runtime/publicPath","webpack://FFmpegWASM/webpack/runtime/jsonp chunk loading","webpack://FFmpegWASM/./dist/esm/const.js","webpack://FFmpegWASM/./dist/esm/utils.js","webpack://FFmpegWASM/./dist/esm/errors.js","webpack://FFmpegWASM/./dist/esm/classes.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"FFmpegWASM\"] = factory();\n\telse\n\t\troot[\"FFmpegWASM\"] = factory();\n})(self, () => {\nreturn ","// The require scope\nvar __webpack_require__ = {};\n\n// expose the modules object (__webpack_modules__)\n__webpack_require__.m = __webpack_modules__;\n\n","// define getter functions for harmony exports\n__webpack_require__.d = (exports, definition) => {\n\tfor(var key in definition) {\n\t\tif(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {\n\t\t\tObject.defineProperty(exports, key, { enumerable: true, get: definition[key] });\n\t\t}\n\t}\n};","// This function allow to reference async chunks\n__webpack_require__.u = (chunkId) => {\n\t// return url for filenames based on template\n\treturn \"\" + chunkId + \".ffmpeg.js\";\n};","__webpack_require__.g = (function() {\n\tif (typeof globalThis === 'object') return globalThis;\n\ttry {\n\t\treturn this || new Function('return this')();\n\t} catch (e) {\n\t\tif (typeof window === 'object') return window;\n\t}\n})();","__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))","// define __esModule on exports\n__webpack_require__.r = (exports) => {\n\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n\t}\n\tObject.defineProperty(exports, '__esModule', { value: true });\n};","var scriptUrl;\nif (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + \"\";\nvar document = __webpack_require__.g.document;\nif (!scriptUrl && document) {\n\tif (document.currentScript)\n\t\tscriptUrl = document.currentScript.src;\n\tif (!scriptUrl) {\n\t\tvar scripts = document.getElementsByTagName(\"script\");\n\t\tif(scripts.length) {\n\t\t\tvar i = scripts.length - 1;\n\t\t\twhile (i > -1 && !scriptUrl) scriptUrl = scripts[i--].src;\n\t\t}\n\t}\n}\n// When supporting browsers where an automatic publicPath is not supported you must specify an output.publicPath manually via configuration\n// or pass an empty string (\"\") and set the __webpack_public_path__ variable from your code to use your own logic.\nif (!scriptUrl) throw new Error(\"Automatic publicPath is not supported in this browser\");\nscriptUrl = scriptUrl.replace(/#.*$/, \"\").replace(/\\?.*$/, \"\").replace(/\\/[^\\/]+$/, \"/\");\n__webpack_require__.p = scriptUrl;","__webpack_require__.b = document.baseURI || self.location.href;\n\n// object to store loaded and loading chunks\n// undefined = chunk not loaded, null = chunk preloaded/prefetched\n// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded\nvar installedChunks = {\n\t179: 0\n};\n\n// no chunk on demand loading\n\n// no prefetching\n\n// no preloaded\n\n// no HMR\n\n// no HMR manifest\n\n// no on chunks loaded\n\n// no jsonp function","export const MIME_TYPE_JAVASCRIPT = \"text/javascript\";\nexport const MIME_TYPE_WASM = \"application/wasm\";\nexport const CORE_VERSION = \"0.12.6\";\nexport const CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;\nexport var FFMessageType;\n(function (FFMessageType) {\n FFMessageType[\"LOAD\"] = \"LOAD\";\n FFMessageType[\"EXEC\"] = \"EXEC\";\n FFMessageType[\"WRITE_FILE\"] = \"WRITE_FILE\";\n FFMessageType[\"READ_FILE\"] = \"READ_FILE\";\n FFMessageType[\"DELETE_FILE\"] = \"DELETE_FILE\";\n FFMessageType[\"RENAME\"] = \"RENAME\";\n FFMessageType[\"CREATE_DIR\"] = \"CREATE_DIR\";\n FFMessageType[\"LIST_DIR\"] = \"LIST_DIR\";\n FFMessageType[\"DELETE_DIR\"] = \"DELETE_DIR\";\n FFMessageType[\"ERROR\"] = \"ERROR\";\n FFMessageType[\"DOWNLOAD\"] = \"DOWNLOAD\";\n FFMessageType[\"PROGRESS\"] = \"PROGRESS\";\n FFMessageType[\"LOG\"] = \"LOG\";\n FFMessageType[\"MOUNT\"] = \"MOUNT\";\n FFMessageType[\"UNMOUNT\"] = \"UNMOUNT\";\n})(FFMessageType || (FFMessageType = {}));\n","/**\n * Generate an unique message ID.\n */\nexport const getMessageID = (() => {\n let messageID = 0;\n return () => messageID++;\n})();\n","export const ERROR_UNKNOWN_MESSAGE_TYPE = new Error(\"unknown message type\");\nexport const ERROR_NOT_LOADED = new Error(\"ffmpeg is not loaded, call `await ffmpeg.load()` first\");\nexport const ERROR_TERMINATED = new Error(\"called FFmpeg.terminate()\");\nexport const ERROR_IMPORT_FAILURE = new Error(\"failed to import ffmpeg-core.js\");\n","import { FFMessageType } from \"./const.js\";\nimport { getMessageID } from \"./utils.js\";\nimport { ERROR_TERMINATED, ERROR_NOT_LOADED } from \"./errors.js\";\n/**\n * Provides APIs to interact with ffmpeg web worker.\n *\n * @example\n * ```ts\n * const ffmpeg = new FFmpeg();\n * ```\n */\nexport class FFmpeg {\n #worker = null;\n /**\n * #resolves and #rejects tracks Promise resolves and rejects to\n * be called when we receive message from web worker.\n */\n #resolves = {};\n #rejects = {};\n #logEventCallbacks = [];\n #progressEventCallbacks = [];\n loaded = false;\n /**\n * register worker message event handlers.\n */\n #registerHandlers = () => {\n if (this.#worker) {\n this.#worker.onmessage = ({ data: { id, type, data }, }) => {\n switch (type) {\n case FFMessageType.LOAD:\n this.loaded = true;\n this.#resolves[id](data);\n break;\n case FFMessageType.MOUNT:\n case FFMessageType.UNMOUNT:\n case FFMessageType.EXEC:\n case FFMessageType.WRITE_FILE:\n case FFMessageType.READ_FILE:\n case FFMessageType.DELETE_FILE:\n case FFMessageType.RENAME:\n case FFMessageType.CREATE_DIR:\n case FFMessageType.LIST_DIR:\n case FFMessageType.DELETE_DIR:\n this.#resolves[id](data);\n break;\n case FFMessageType.LOG:\n this.#logEventCallbacks.forEach((f) => f(data));\n break;\n case FFMessageType.PROGRESS:\n this.#progressEventCallbacks.forEach((f) => f(data));\n break;\n case FFMessageType.ERROR:\n this.#rejects[id](data);\n break;\n }\n delete this.#resolves[id];\n delete this.#rejects[id];\n };\n }\n };\n /**\n * Generic function to send messages to web worker.\n */\n #send = ({ type, data }, trans = [], signal) => {\n if (!this.#worker) {\n return Promise.reject(ERROR_NOT_LOADED);\n }\n return new Promise((resolve, reject) => {\n const id = getMessageID();\n this.#worker && this.#worker.postMessage({ id, type, data }, trans);\n this.#resolves[id] = resolve;\n this.#rejects[id] = reject;\n signal?.addEventListener(\"abort\", () => {\n reject(new DOMException(`Message # ${id} was aborted`, \"AbortError\"));\n }, { once: true });\n });\n };\n on(event, callback) {\n if (event === \"log\") {\n this.#logEventCallbacks.push(callback);\n }\n else if (event === \"progress\") {\n this.#progressEventCallbacks.push(callback);\n }\n }\n off(event, callback) {\n if (event === \"log\") {\n this.#logEventCallbacks = this.#logEventCallbacks.filter((f) => f !== callback);\n }\n else if (event === \"progress\") {\n this.#progressEventCallbacks = this.#progressEventCallbacks.filter((f) => f !== callback);\n }\n }\n /**\n * Loads ffmpeg-core inside web worker. It is required to call this method first\n * as it initializes WebAssembly and other essential variables.\n *\n * @category FFmpeg\n * @returns `true` if ffmpeg core is loaded for the first time.\n */\n load = ({ classWorkerURL, ...config } = {}, { signal } = {}) => {\n if (!this.#worker) {\n this.#worker = classWorkerURL ?\n new Worker(new URL(classWorkerURL, import.meta.url), {\n type: \"module\",\n }) :\n // We need to duplicated the code here to enable webpack\n // to bundle worekr.js here.\n new Worker(new URL(\"./worker.js\", import.meta.url), {\n type: \"module\",\n });\n this.#registerHandlers();\n }\n return this.#send({\n type: FFMessageType.LOAD,\n data: config,\n }, undefined, signal);\n };\n /**\n * Execute ffmpeg command.\n *\n * @remarks\n * To avoid common I/O issues, [\"-nostdin\", \"-y\"] are prepended to the args\n * by default.\n *\n * @example\n * ```ts\n * const ffmpeg = new FFmpeg();\n * await ffmpeg.load();\n * await ffmpeg.writeFile(\"video.avi\", ...);\n * // ffmpeg -i video.avi video.mp4\n * await ffmpeg.exec([\"-i\", \"video.avi\", \"video.mp4\"]);\n * const data = ffmpeg.readFile(\"video.mp4\");\n * ```\n *\n * @returns `0` if no error, `!= 0` if timeout (1) or error.\n * @category FFmpeg\n */\n exec = (\n /** ffmpeg command line args */\n args, \n /**\n * milliseconds to wait before stopping the command execution.\n *\n * @defaultValue -1\n */\n timeout = -1, { signal } = {}) => this.#send({\n type: FFMessageType.EXEC,\n data: { args, timeout },\n }, undefined, signal);\n /**\n * Terminate all ongoing API calls and terminate web worker.\n * `FFmpeg.load()` must be called again before calling any other APIs.\n *\n * @category FFmpeg\n */\n terminate = () => {\n const ids = Object.keys(this.#rejects);\n // rejects all incomplete Promises.\n for (const id of ids) {\n this.#rejects[id](ERROR_TERMINATED);\n delete this.#rejects[id];\n delete this.#resolves[id];\n }\n if (this.#worker) {\n this.#worker.terminate();\n this.#worker = null;\n this.loaded = false;\n }\n };\n /**\n * Write data to ffmpeg.wasm.\n *\n * @example\n * ```ts\n * const ffmpeg = new FFmpeg();\n * await ffmpeg.load();\n * await ffmpeg.writeFile(\"video.avi\", await fetchFile(\"../video.avi\"));\n * await ffmpeg.writeFile(\"text.txt\", \"hello world\");\n * ```\n *\n * @category File System\n */\n writeFile = (path, data, { signal } = {}) => {\n const trans = [];\n if (data instanceof Uint8Array) {\n trans.push(data.buffer);\n }\n return this.#send({\n type: FFMessageType.WRITE_FILE,\n data: { path, data },\n }, trans, signal);\n };\n mount = (fsType, options, mountPoint) => {\n const trans = [];\n return this.#send({\n type: FFMessageType.MOUNT,\n data: { fsType, options, mountPoint },\n }, trans);\n };\n unmount = (mountPoint) => {\n const trans = [];\n return this.#send({\n type: FFMessageType.UNMOUNT,\n data: { mountPoint },\n }, trans);\n };\n /**\n * Read data from ffmpeg.wasm.\n *\n * @example\n * ```ts\n * const ffmpeg = new FFmpeg();\n * await ffmpeg.load();\n * const data = await ffmpeg.readFile(\"video.mp4\");\n * ```\n *\n * @category File System\n */\n readFile = (path, \n /**\n * File content encoding, supports two encodings:\n * - utf8: read file as text file, return data in string type.\n * - binary: read file as binary file, return data in Uint8Array type.\n *\n * @defaultValue binary\n */\n encoding = \"binary\", { signal } = {}) => this.#send({\n type: FFMessageType.READ_FILE,\n data: { path, encoding },\n }, undefined, signal);\n /**\n * Delete a file.\n *\n * @category File System\n */\n deleteFile = (path, { signal } = {}) => this.#send({\n type: FFMessageType.DELETE_FILE,\n data: { path },\n }, undefined, signal);\n /**\n * Rename a file or directory.\n *\n * @category File System\n */\n rename = (oldPath, newPath, { signal } = {}) => this.#send({\n type: FFMessageType.RENAME,\n data: { oldPath, newPath },\n }, undefined, signal);\n /**\n * Create a directory.\n *\n * @category File System\n */\n createDir = (path, { signal } = {}) => this.#send({\n type: FFMessageType.CREATE_DIR,\n data: { path },\n }, undefined, signal);\n /**\n * List directory contents.\n *\n * @category File System\n */\n listDir = (path, { signal } = {}) => this.#send({\n type: FFMessageType.LIST_DIR,\n data: { path },\n }, undefined, signal);\n /**\n * Delete an empty directory.\n *\n * @category File System\n */\n deleteDir = (path, { signal } = {}) => this.#send({\n type: FFMessageType.DELETE_DIR,\n data: { path },\n }, undefined, signal);\n}\n"],"names":["root","factory","exports","module","define","amd","self","__webpack_require__","definition","key","o","Object","defineProperty","enumerable","get","chunkId","g","globalThis","this","Function","e","window","obj","prop","prototype","hasOwnProperty","call","r","Symbol","toStringTag","value","scriptUrl","importScripts","location","document","currentScript","src","scripts","getElementsByTagName","length","i","Error","replace","p","b","baseURI","href","FFMessageType","getMessageID","messageID","ERROR_NOT_LOADED","ERROR_TERMINATED","FFmpeg","loaded","onmessage","data","id","type","LOAD","MOUNT","UNMOUNT","EXEC","WRITE_FILE","READ_FILE","DELETE_FILE","RENAME","CREATE_DIR","LIST_DIR","DELETE_DIR","LOG","forEach","f","PROGRESS","ERROR","trans","signal","Promise","resolve","reject","postMessage","addEventListener","DOMException","once","on","event","callback","push","off","filter","load","classWorkerURL","config","Worker","URL","undefined","exec","args","timeout","terminate","ids","keys","writeFile","path","Uint8Array","buffer","mount","fsType","options","mountPoint","unmount","readFile","encoding","deleteFile","rename","oldPath","newPath","createDir","listDir","deleteDir"],"sourceRoot":""} \ No newline at end of file diff --git a/data/config.toml b/data/config.toml index 7ad6fad..162c37c 100644 --- a/data/config.toml +++ b/data/config.toml @@ -1,22 +1,22 @@ [app] -app_url = "http://127.0.0.1:8000" +app_url = "http://192.168.1.168:8000/" app_key = "grok2api" -api_key = "" +api_key = "sk-3b5b3bd501a145789e02462c351a6051" public_enabled = true public_key = "" image_format = "url" video_format = "html" temporary = true disable_memory = true -stream = true -thinking = true +stream = false +thinking = false dynamic_statsig = true filter_tags = ["xaiartifact","xai:tool_usage_card","grok:render"] [proxy] base_proxy_url = "" asset_proxy_url = "" -cf_clearance = "" +cf_clearance = "TZDYXNr3xjKU918mEpvp4pZ7GdWfWAeVWYdr25ZBFZk-1771200552-1.2.1.1-nBA2kDmJbeOzB4LtkWmrIRlSg8J8PgCeTS822.l9hFN6rQ7oximaVJ59rJN1eXhjuF_4qvCDQNtkHojrkNXMkP4lkHH.KvhUqB1Q7jwGFvgBPWp.ZDgHV.zYRUznVSQQnoyo9XNnyUN_wGFLOmj8rZDhV2pFXuT14E4rGO1ULTjC297..CBvH2K7fLpBAJWnzZpskPec3mX4fsnbn9ZKs3mW9YU__PkTp.TWcxINYGU" browser = "chrome136" user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36" @@ -60,6 +60,10 @@ stream_timeout = 60 [voice] timeout = 120 +livekit_url = "" +livekit_urls = [] +signal_proxy_enabled = true +signal_proxy_url = "" [asset] upload_concurrent = 30 diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh old mode 100755 new mode 100644 diff --git a/scripts/init_storage.sh b/scripts/init_storage.sh old mode 100755 new mode 100644