WWW.YOUINFO.SITE
标签聚合 Horse

/tag/Horse

LinuxDo 最新话题 · 2026-05-29 21:32:09+08:00 · tech

是这样的,前段时间参加快乐马的活动,领取了1万5的积分。但是自己因为工作的原因,实在是太忙了,没有时间去好好地琢磨。那下个月12号就到期了。嗯,如果说有需要的伙伴可以评论联系我,我登在你的上面。虽然说有点麻烦啊,因为这个不是API,你可以登我的号去使用。相信各位的人品,有需要的可以联系我。或者有新手的话,想学习做视频,拿去学习也可以,拿去练习也可以,没关系的,我是真的用不上。要不然浪费了。 2 个帖子 - 1 位参与者 阅读完整话题

IT之家 · 2026-05-24 09:18:20+08:00 · tech

IT之家 5 月 24 日消息,《天国》系列开发商战马工作室(Warhorse)5 月 20 日确认有两款新作在研。 战马工作室不仅正在开发一款全新的“中土世界”题材开放世界游戏,同时还在推进另一个《天国》项目。当时许多玩家担心《天国》系列将被搁置,以便为中土世界让路。 不过,由于此前的公告中仅提及新作是一款《天国》题材的“冒险”,并未明确说明是游戏还是影视剧,外界一度产生困惑。很快,母公司 Embracer 澄清该项目确实是一款传统意义上的电子游戏。 随后,战马工作室公关负责人 Tobias Stolz-Zwilling 在社区直播中确认,前几日公开的两个新项目 ——《天国:拯救》新作和中土世界题材游戏均为“开放世界 RPG”。 他还透露,《天国:拯救 2》的设计师 Prokop Jirsa 将接替工作室联合创始人 Daniel Vávra 担任该系列创意负责人,后者已转向电影或电视剧改编项目。与此同时,工作室资深成员、《天国:拯救 2》设计总监 Viktor Bocan 将领导中土世界团队。Stolz-Zwilling 重申了 Embracer 此前的说法:“如果一切顺利”,《天国》新作将在下一个财年推出(IT之家注:2027 年 4 月至 2028 年 3 月)。

IT之家 · 2026-05-20 22:26:43+08:00 · tech

IT之家 5 月 20 日消息,据外媒 VGC 今晚报道,《天国:拯救》开发商 Warhorse Studios 正式确认,一款以《指环王》中“中土世界”为背景的开放世界 RPG 已经进入开发阶段。“你们可能已经听过那些传闻了,现在也是时候公开我们正在制作的内容了。” 除了全新的“中土世界”RPG 外,Warhorse 同时还宣布《天国:拯救》系列将推出新作。据IT之家了解, “中土世界”是 J.R.R . 托尔金《指环王》系列的核心世界观 ,也是整个奇幻文学领域最经典的设定之一。 Warhorse Studios 隶属于 Embracer Group。Embracer Group 还宣布成立新公司 Fellowship Entertainment,未来将负责集团核心 IP 的开发、发行与授权业务。 目前纳入 Fellowship Entertainment 体系的重要 IP 包括 《古墓丽影》《暗黑血统》《死亡岛》《天国:拯救》《地铁》以及《指环王》等 。 另外,《天国:拯救》导演丹尼尔 · 瓦夫拉今年早些时候已经暂时离开游戏开发工作,转而投入《天国:拯救》真人电影项目。

v2ex · 2026-05-19 20:03:49+08:00 · tech

大家好,分享一个我最近在做的项目: FastMoro AI ( fastmoroai.com ) 简单来说,就是把目前主流的 AI 视频和图像模型整合到一个平台里,不用到处切换账号、不用管 API 配额,一个地方搞定。 集成了哪些模型?能做什么? 平台集成了 4 款主流 AI 模型,覆盖以下功能场景: 🎬 AI 视频生成 文本转视频 — 输入文字描述,直接生成电影级视频 图片转视频 — 上传静态图片,AI 让它自然动起来 参考视频 — 上传参考图像/视频,控制风格、色彩和运动 驱动模型: Veo 3.1 (高保真画质) 、 Seedance 2.0 (动态运动效果) 、 Happy Horse 1.0 (视频生成) 🖼️ AI 图像生成与编辑 文本转图像 — 文字描述生成高清图像 图像编辑 — 换背景、改风格、移除物体,一句话搞定 ChatGPT Image 2 — 精准文字渲染,海报/封面直接出图 驱动模型:ChatGPT Image 2.0 (文字渲染、图像生成) 后续还会持续跟进接入新的 AI 模型,一个平台用到最新的模型能力 🚀 一个账号、统一积分体系,不用在多个平台之间来回切换。 为什么做这个? 之前自己在用各种 AI 视觉工具的时候,最大的痛点就是:想用好一点的模型得在好几个平台之间来回切,有的要排 waitlist ,有的 API 配置一堆,有的免费额度用完就不记得续了。 就想着干脆把主流模型整合到一起,统一界面、统一积分体系, 一个账号能用到多个模型 ,也不用操心各个平台的额度管理。 关于免费和积分 平台基础功能是免费的,有赠送积分,日常简单用不太需要付费。这次上线也准备了一些活动: 🎉 上线活动 为了感谢第一批用户的支持,准备了以下福利: 活动 内容 🥇 前 10 名注册用户 赠送 100 积分 ,直接到账 🥈 前 100 名注册用户 享 7 折优惠 (积分充值永久有效) 💡 提建议送积分 在帖子里提有价值的建议/问题,每个问题再送 50 积分 活动说明:注册后回复本帖即可,我会按回帖顺序确认活动资格。提建议送积分长期有效,不限前多少名。 适合什么场景? 想做 AI 视频但不想折腾多个平台 需要对比不同模型生成效果的创作者 图像生成 + 编辑一体化需求(换背景、改风格、去物体等) 单纯想免费体验目前主流的 AI 视频模型 目前平台刚上线,肯定还有很多不完善的地方。如果遇到 bug 或者有什么功能想法,欢迎直接回帖,每条我都会看,有价值的建议直接送积分 🫡 链接: https://fastmoroai.com/

v2ex · 2026-05-18 00:03:56+08:00 · tech

最近折腾了一个有点好玩的站: happyhorse.space (名字就很乐观),简单介绍一下,顺便和大家分享下做这个小玩具的思路和技术细节。 这个站是干嘛的? 一句话概括:这是一个围绕「马」这个主题做的轻量小站,主要就是图一乐、解压、随便点点看看。 目前大致可以: 看一些和「马」相关的内容(图片 / 文本 / 小互动) 做了一点和随机性、情绪相关的设计,每次打开都有点不一样 不追求“刚需”和“效率”,更偏「好玩」「实验」 技术栈和实现 整体比较克制,没有上太重的东西: 前端是静态页面 + 少量 JS ,尽量保证首屏打开速度 后端/部署用的是静态托管 + HTTPS (比如 Vercel / Netlify / Nginx 这一类) 做了一些简单缓存策略,顺手优化了下移动端体验和小动画效果 如果有人感兴趣的话,后面可以单独拆一贴写下实现细节。 为什么要做这个? 纯粹是想做一个 不以效率和产出为目标 的小项目: 写业务代码写多了,想搞个轻松一点的玩具 试试用一个几乎没有刚需的主题,能不能靠氛围感和细节留住一点点访问 自己偶尔也需要一个可以随便点开笑一下的页面 最后 感兴趣的可以随便逛逛: happyhorse.space 有任何吐槽、想法或者觉得可以加点什么功能,欢迎在评论区拍砖。

v2ex · 2026-05-18 00:03:56+08:00 · tech

最近折腾了一个有点好玩的站: happyhorse.space (名字就很乐观),简单介绍一下,顺便和大家分享下做这个小玩具的思路和技术细节。 这个站是干嘛的? 一句话概括:这是一个围绕「马」这个主题做的轻量小站,主要就是图一乐、解压、随便点点看看。 目前大致可以: 看一些和「马」相关的内容(图片 / 文本 / 小互动) 做了一点和随机性、情绪相关的设计,每次打开都有点不一样 不追求“刚需”和“效率”,更偏「好玩」「实验」 技术栈和实现 整体比较克制,没有上太重的东西: 前端是静态页面 + 少量 JS ,尽量保证首屏打开速度 后端/部署用的是静态托管 + HTTPS (比如 Vercel / Netlify / Nginx 这一类) 做了一些简单缓存策略,顺手优化了下移动端体验和小动画效果 如果有人感兴趣的话,后面可以单独拆一贴写下实现细节。 为什么要做这个? 纯粹是想做一个 不以效率和产出为目标 的小项目: 写业务代码写多了,想搞个轻松一点的玩具 试试用一个几乎没有刚需的主题,能不能靠氛围感和细节留住一点点访问 自己偶尔也需要一个可以随便点开笑一下的页面 最后 感兴趣的可以随便逛逛: happyhorse.space 有任何吐槽、想法或者觉得可以加点什么功能,欢迎在评论区拍砖。

v2ex · 2026-05-18 00:03:56+08:00 · tech

最近折腾了一个有点好玩的站: happyhorse.space (名字就很乐观),简单介绍一下,顺便和大家分享下做这个小玩具的思路和技术细节。 这个站是干嘛的? 一句话概括:这是一个围绕「马」这个主题做的轻量小站,主要就是图一乐、解压、随便点点看看。 目前大致可以: 看一些和「马」相关的内容(图片 / 文本 / 小互动) 做了一点和随机性、情绪相关的设计,每次打开都有点不一样 不追求“刚需”和“效率”,更偏「好玩」「实验」 技术栈和实现 整体比较克制,没有上太重的东西: 前端是静态页面 + 少量 JS ,尽量保证首屏打开速度 后端/部署用的是静态托管 + HTTPS (比如 Vercel / Netlify / Nginx 这一类) 做了一些简单缓存策略,顺手优化了下移动端体验和小动画效果 如果有人感兴趣的话,后面可以单独拆一贴写下实现细节。 为什么要做这个? 纯粹是想做一个 不以效率和产出为目标 的小项目: 写业务代码写多了,想搞个轻松一点的玩具 试试用一个几乎没有刚需的主题,能不能靠氛围感和细节留住一点点访问 自己偶尔也需要一个可以随便点开笑一下的页面 最后 感兴趣的可以随便逛逛: happyhorse.space 有任何吐槽、想法或者觉得可以加点什么功能,欢迎在评论区拍砖。

linux.do · 2026-04-29 16:37:58+08:00 · tech

mimo-v2.5-pro写的,有些小瑕疵,问题不大。感兴趣的自己改改。 演示: 千问视频无水印下载 js版: /** * 通义千问 AI 视频无水印下载 - Cloudflare Worker * * 支持两种分享链接: * 类型1: https://www.qianwen.com/share/chat/{id} * 类型2: https://activity.qianwen.com/r/ai-studio-mobile/qwen-external-share?shareId=xxx&authorId=xxx * * 路由: * GET / → 前端 UI * POST /api/parse → 解析分享链接 * GET /api/proxy → 代理下载 */ export default { async fetch(request, env) { const url = new URL(request.url); if (request.method === "GET" && url.pathname === "/") { return new Response(HTML_PAGE, { headers: { "Content-Type": "text/html; charset=utf-8" }, }); } if (request.method === "POST" && url.pathname === "/api/parse") { return handleParse(request); } if (request.method === "GET" && url.pathname === "/api/proxy") { return handleProxy(url); } // CORS preflight if (request.method === "OPTIONS") { return new Response(null, { headers: { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, POST, OPTIONS", "Access-Control-Allow-Headers": "Content-Type", }, }); } return new Response("Not Found", { status: 404 }); }, }; // ── 解析入口 ────────────────────────────────────────────── async function handleParse(request) { try { const { url } = await request.json(); const linkInfo = detectShareType(url); let result; if (linkInfo.type === "qianwen_chat") { result = await fetchType1(linkInfo.shareId); } else { result = await fetchType2(linkInfo.shareId, linkInfo.authorId); } return jsonResponse(result); } catch (e) { return jsonResponse({ error: e.message }, 500); } } // ── URL 识别 ────────────────────────────────────────────── function detectShareType(url) { // 类型1: www.qianwen.com/share/chat/{id} const m1 = url.match(/\/share\/chat\/([a-f0-9]+)/); if (m1) return { type: "qianwen_chat", shareId: m1[1] }; // 类型2: activity.qianwen.com/...?shareId=xxx&authorId=xxx if (url.includes("activity.qianwen.com")) { const u = new URL(url); const shareId = u.searchParams.get("shareId"); const authorId = u.searchParams.get("authorId") || ""; if (shareId) return { type: "activity_share", shareId, authorId }; } throw new Error("无法识别的链接格式,请粘贴千问分享链接"); } // ── 类型1: chat2-api ────────────────────────────────────── async function fetchType1(shareId) { const resp = await fetch( "https://chat2-api.qianwen.com/api/v1/share/info?pr=qwen&fr=web", { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": UA, Referer: "https://www.qianwen.com/", }, body: JSON.stringify({ share_id: shareId, biz_id: "ai_qwen" }), } ); if (!resp.ok) throw new Error(`chat2-api 返回 ${resp.status}`); const data = await resp.json(); if (data.code !== 0) throw new Error(data.msg || "API 错误"); return parseType1(data.data); } function parseType1(apiData) { const title = apiData.title || "未知"; const records = apiData.session?.record_list || []; for (const record of records) { for (const msg of record.response_messages || []) { for (const item of msg.meta_data?.multi_load || []) { if (item.type !== "ai_generate_video") continue; const content = item.content || {}; const infos = content.resource_infos || []; const layout = (content.layout_list || [])[0] || {}; const displayRefs = layout.video || []; const downloadRefs = layout.download_video || []; const coverRefs = layout.cover || []; const resMap = {}; for (const r of infos) resMap[r.refer_id] = r; const extra = content.extra_info?.content?.extra || {}; const all = []; // video = 无水印 for (const id of displayRefs) { if (resMap[id]?.url?.includes(".mp4")) all.push({ ...resMap[id], label: "无水印", type: "video" }); } // download_video = 有水印 for (const id of downloadRefs) { if (resMap[id]?.url?.includes(".mp4")) all.push({ ...resMap[id], label: "有水印", type: "video" }); } // 封面 for (const id of coverRefs) { if (resMap[id]) all.push({ ...resMap[id], label: "封面", type: "image" }); } // 其他 const known = new Set([...displayRefs, ...downloadRefs, ...coverRefs]); for (const r of infos) { if (!known.has(r.refer_id)) all.push({ ...r, label: r.url?.includes(".png") ? "原图" : "其他", type: "image" }); } return { source: "qianwen_chat", title, model: extra.model_name || content.generate_model_name || "", scene: content.scene || "", duration: content.duration || 0, resolution: content.ratio || "", prompt: extra.query || content.prompt || "", allResources: all, }; } } } throw new Error("未找到视频资源"); } // ── 类型2: zaodian ──────────────────────────────────────── async function fetchType2(shareId, authorId) { const resp = await fetch( "https://qwen-api.zaodian.com/api/v1/share/get", { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": UA, Referer: "https://activity.qianwen.com/", }, body: JSON.stringify({ shareId, authorId, chid: "null", product: "ai_studio", }), } ); if (!resp.ok) throw new Error(`zaodian-api 返回 ${resp.status}`); const data = await resp.json(); if (data.code !== 0) throw new Error(data.msg || "API 错误"); return parseType2(data.data); } function parseType2(data) { const title = data.title || data.shareTitle || "未知"; const prompt = data.content?.prompt || ""; const scene = data.objectType || ""; const play = data.playInfo || {}; const img = data.image || {}; const all = []; if (play.url) all.push({ refer_id: "video_main", url: play.url, label: "视频(无水印)", type: "video" }); if (play.playUrl) all.push({ refer_id: "video_play", url: play.playUrl, label: "视频(预览)", type: "video" }); if (play.downloadUrl) all.push({ refer_id: "video_download", url: play.downloadUrl, label: "视频(有水印)", type: "video" }); if (img.url) all.push({ refer_id: "cover", url: img.url, width: img.width, height: img.height, label: "封面", type: "image" }); return { source: "activity_share", title, model: "", scene, duration: play.videoTotalTime || 0, resolution: "", prompt, allResources: all, }; } // ── 代理下载 ────────────────────────────────────────────── const UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; const ALLOWED_DOMAINS = [ "workspace-zb-cdn.quark.cn", "quark-aistudio-cdn.quark.cn", "img.alicdn.com", ]; async function handleProxy(url) { const target = url.searchParams.get("u"); if (!target) return new Response("Missing u param", { status: 400 }); const parsed = new URL(target); if (!ALLOWED_DOMAINS.some((d) => parsed.hostname.endsWith(d))) { return new Response("Domain not allowed", { status: 403 }); } const upstream = await fetch(target, { headers: { "User-Agent": UA, Referer: "https://www.qianwen.com/" }, }); if (!upstream.ok) return new Response(`Upstream ${upstream.status}`, { status: upstream.status }); const headers = new Headers(); headers.set("Content-Type", upstream.headers.get("Content-Type") || "application/octet-stream"); headers.set("Content-Disposition", 'attachment'); headers.set("Cache-Control", "public, max-age=3600"); headers.set("Access-Control-Allow-Origin", "*"); return new Response(upstream.body, { headers }); } // ── 工具 ────────────────────────────────────────────────── function jsonResponse(data, status = 200) { return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", }, }); } // ── 前端 HTML ───────────────────────────────────────────── const HTML_PAGE = `<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1"/> <title>千问视频无水印下载</title> <style> *{margin:0;padding:0;box-sizing:border-box} body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; background:#0a0a0a;color:#e0e0e0;min-height:100vh;display:flex;flex-direction:column;align-items:center} .container{max-width:720px;width:100%;padding:24px 16px} h1{font-size:1.6rem;font-weight:700;text-align:center;margin-bottom:4px; background:linear-gradient(135deg,#6366f1,#a855f7,#ec4899);-webkit-background-clip:text; -webkit-text-fill-color:transparent} .subtitle{text-align:center;color:#888;font-size:.85rem;margin-bottom:28px} .input-group{display:flex;gap:8px;margin-bottom:20px} input[type=text]{flex:1;padding:12px 16px;border-radius:10px;border:1px solid #2a2a2a; background:#141414;color:#fff;font-size:.95rem;outline:none;transition:border .2s} input[type=text]:focus{border-color:#6366f1} input[type=text]::placeholder{color:#555} button{padding:12px 24px;border-radius:10px;border:none;font-weight:600;font-size:.95rem; cursor:pointer;transition:all .2s} .btn-primary{background:linear-gradient(135deg,#6366f1,#a855f7);color:#fff} .btn-primary:hover{opacity:.9;transform:translateY(-1px)} .btn-primary:disabled{opacity:.4;cursor:not-allowed;transform:none} .btn-download{background:linear-gradient(135deg,#10b981,#06b6d4);color:#fff; padding:10px 20px;font-size:.85rem} .btn-download:hover{opacity:.9} .btn-download:disabled{opacity:.4;cursor:not-allowed} .examples{margin-bottom:20px;padding:12px 14px;background:#111;border:1px solid #1a1a1a; border-radius:10px;font-size:.78rem;color:#666;line-height:1.8} .examples b{color:#888} .examples code{color:#a78bfa;background:#1a1028;padding:1px 5px;border-radius:4px;font-size:.76rem} .status{text-align:center;padding:20px;color:#888;font-size:.9rem} .spinner{display:inline-block;width:20px;height:20px;border:2px solid #333; border-top-color:#6366f1;border-radius:50%;animation:spin .6s linear infinite; vertical-align:middle;margin-right:8px} @keyframes spin{to{transform:rotate(360deg)}} .error{color:#f87171;text-align:center;padding:16px;background:#1a0a0a; border-radius:10px;margin-bottom:16px;border:1px solid #3a1a1a} .info-card{background:#141414;border:1px solid #2a2a2a;border-radius:14px; padding:20px;margin-bottom:20px} .info-card h2{font-size:1.1rem;margin-bottom:12px;color:#fff} .info-row{display:flex;gap:8px;margin-bottom:6px;font-size:.85rem} .info-label{color:#888;min-width:60px} .info-value{color:#ccc;word-break:break-all} .source-tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.7rem; font-weight:600;margin-left:8px;vertical-align:middle} .source-qwen{background:#1a1a2a;color:#818cf8;border:1px solid #2a2a4a} .source-activity{background:#1a2a1a;color:#34d399;border:1px solid #2a4a2a} .prompt-text{color:#aaa;font-size:.82rem;line-height:1.5;margin-top:8px; padding:10px;background:#0f0f0f;border-radius:8px;max-height:80px;overflow-y:auto} .resources{margin-top:16px} .resources h3{font-size:.95rem;margin-bottom:12px;color:#ddd} .resource-item{display:flex;align-items:center;gap:12px;padding:12px 14px; background:#141414;border:1px solid #2a2a2a;border-radius:10px;margin-bottom:8px; transition:border-color .2s;cursor:pointer} .resource-item:hover{border-color:#3a3a3a} .resource-item.selected{border-color:#6366f1;background:#14142a} .resource-item input[type=checkbox]{width:18px;height:18px;accent-color:#6366f1; cursor:pointer;flex-shrink:0} .resource-thumb{width:56px;height:56px;border-radius:8px;object-fit:cover; background:#1a1a1a;flex-shrink:0} .resource-info{flex:1;min-width:0} .resource-name{font-size:.85rem;color:#ddd;font-weight:500; overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .resource-meta{font-size:.75rem;color:#888;margin-top:2px} .tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:.7rem; font-weight:600;margin-left:6px} .tag-no-wm{background:#0a2a1a;color:#34d399;border:1px solid #1a4a2a} .tag-wm{background:#2a1a0a;color:#fbbf24;border:1px solid #4a2a1a} .tag-dl{background:#0a1a2a;color:#60a5fa;border:1px solid #1a2a4a} .tag-play{background:#2a0a2a;color:#c084fc;border:1px solid #4a1a4a} .tag-cover{background:#1a1a2a;color:#818cf8;border:1px solid #2a2a4a} .tag-other{background:#1a1a1a;color:#888;border:1px solid #2a2a2a} .action-bar{display:flex;align-items:center;justify-content:space-between; margin-top:16px;padding:12px 0;border-top:1px solid #2a2a2a} .select-all{display:flex;align-items:center;gap:8px;font-size:.85rem;color:#aaa;cursor:pointer} .select-all input{accent-color:#6366f1} .footer{text-align:center;color:#444;font-size:.75rem;margin-top:auto;padding:20px 0} @media(max-width:480px){ .container{padding:16px 12px} h1{font-size:1.3rem} .input-group{flex-direction:column} .btn-primary{width:100%} } </style> </head> <body> <div class="container"> <h1>🎬 千问视频无水印下载</h1> <p class="subtitle">支持两种千问分享链接,解析并下载无水印视频</p> <div class="examples"> <b>支持格式:</b><br/> ① <code>https://www.qianwen.com/share/chat/xxx</code><br/> ② <code>https://activity.qianwen.com/r/ai-studio-mobile/qwen-external-share?shareId=xxx&authorId=xxx</code> </div> <div class="input-group"> <input type="text" id="urlInput" placeholder="粘贴千问分享链接..." onkeydown="if(event.key==='Enter')parseUrl()"/> <button class="btn-primary" id="parseBtn" onclick="parseUrl()">解析</button> </div> <div id="errorBox" class="error" style="display:none"></div> <div id="loadingBox" class="status" style="display:none"> <span class="spinner"></span>正在解析... </div> <div id="resultBox" style="display:none"></div> </div> <div class="footer">仅供学习交流 · 资源版权归原作者所有</div> <script> const BASE = location.origin; async function parseUrl() { const input = document.getElementById("urlInput"); const btn = document.getElementById("parseBtn"); const errBox = document.getElementById("errorBox"); const loadBox = document.getElementById("loadingBox"); const resBox = document.getElementById("resultBox"); const url = input.value.trim(); if (!url) { showErr("请输入链接"); return; } errBox.style.display = "none"; resBox.style.display = "none"; loadBox.style.display = "block"; btn.disabled = true; try { const resp = await fetch(BASE + "/api/parse", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url }), }); const data = await resp.json(); if (data.error) { showErr(data.error); return; } renderResult(data); } catch (e) { showErr("请求失败: " + e.message); } finally { loadBox.style.display = "none"; btn.disabled = false; } } function showErr(msg) { const box = document.getElementById("errorBox"); box.textContent = msg; box.style.display = "block"; document.getElementById("loadingBox").style.display = "none"; } function esc(s) { if (!s) return ""; const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } function renderResult(data) { const box = document.getElementById("resultBox"); const all = data.allResources || []; let html = ""; // 信息卡片 const srcTag = data.source === "activity_share" ? '<span class="source-tag source-activity">Activity</span>' : '<span class="source-tag source-qwen">千问</span>'; html += '<div class="info-card">'; html += '<h2>' + esc(data.title) + srcTag + '</h2>'; if (data.model) html += row("模型", data.model); if (data.scene) html += row("场景", data.scene); if (data.duration) html += row("时长", data.duration + "秒"); if (data.resolution) html += row("分辨率", data.resolution); if (data.prompt) html += '<div class="prompt-text">💬 ' + esc(data.prompt) + '</div>'; html += '</div>'; // 资源列表 html += '<div class="resources">'; html += '<h3>📦 可下载资源 (' + all.length + ')</h3>'; const tagMap = { "无水印": "tag-no-wm", "有水印": "tag-wm", "视频(有水印)": "tag-dl", "视频(预览)": "tag-play", "视频(无水印)": "tag-play", "封面": "tag-cover", "原图": "tag-cover" }; all.forEach((r, i) => { const tc = tagMap[r.label] || "tag-other"; const ext = r.type === "video" ? "MP4" : r.url?.includes(".png") ? "PNG" : "JPG"; const sz = r.width && r.height ? r.width + "×" + r.height : ""; const thumb = BASE + "/api/proxy?u=" + encodeURIComponent( r.url?.includes(".mp4") ? r.url.split("?")[0] + "?x-oss-process=video/snapshot,t_1000,f_jpg" : r.url ); const autoCheck = (r.label === "无水印" || r.label === "视频(有水印)" || r.label === "视频(无水印)") ? " checked" : ""; html += '<label class="resource-item" id="item-' + i + '">'; html += '<input type="checkbox" data-idx="' + i + '" onchange="updSel()"' + autoCheck + '/>'; html += '<img class="resource-thumb" src="' + thumb + '" onerror="this.style.display=\\'none\\'" loading="lazy"/>'; html += '<div class="resource-info">'; html += '<div class="resource-name">' + esc(r.refer_id) + " · " + ext + ' <span class="tag ' + tc + '">' + esc(r.label) + '</span></div>'; html += '<div class="resource-meta">' + sz + (r.md5 ? " · " + r.md5.slice(0,8) : "") + '</div>'; html += '</div></label>'; }); html += '</div>'; // 操作栏 html += '<div class="action-bar">'; html += '<label class="select-all"><input type="checkbox" id="selAll" onchange="toggleAll()"/> 全选</label>'; html += '<button class="btn-download" id="dlBtn" onclick="dlSel()" disabled>下载选中 (0)</button>'; html += '</div>'; box.innerHTML = html; box.style.display = "block"; window._res = all; updSel(); } function row(label, value) { return '<div class="info-row"><span class="info-label">' + label + '</span><span class="info-value">' + esc(value) + '</span></div>'; } function toggleAll() { const ck = document.getElementById("selAll").checked; document.querySelectorAll('.resource-item input[type=checkbox]').forEach(c => c.checked = ck); updSel(); } function updSel() { const cbs = document.querySelectorAll('.resource-item input[type=checkbox]'); let n = 0; cbs.forEach(c => { const el = document.getElementById("item-" + c.dataset.idx); if (c.checked) { n++; el.classList.add("selected"); } else { el.classList.remove("selected"); } }); const btn = document.getElementById("dlBtn"); btn.textContent = "下载选中 (" + n + ")"; btn.disabled = n === 0; document.getElementById("selAll").checked = n === cbs.length && cbs.length > 0; } async function dlSel() { const resources = window._res || []; const cbs = document.querySelectorAll('.resource-item input[type=checkbox]:checked'); for (const cb of cbs) { const r = resources[cb.dataset.idx]; if (!r?.url) continue; const ext = r.type === "video" ? "mp4" : (r.url?.match(/\\.([^?.]+)/)?.[1] || "jpg"); const proxyUrl = BASE + "/api/proxy?u=" + encodeURIComponent(r.url); const a = document.createElement("a"); a.href = proxyUrl; a.download = "qianwen_" + r.refer_id + "." + ext; a.target = "_blank"; document.body.appendChild(a); a.click(); document.body.removeChild(a); await new Promise(r => setTimeout(r, 500)); } } </script> </body> </html>`; Python版: #!/usr/bin/env python3 """ 通义千问(Qianwen) AI 生成视频无水印下载工具 支持两种分享链接: 类型1: https://www.qianwen.com/share/chat/{id} 类型2: https://activity.qianwen.com/r/ai-studio-mobile/qwen-external-share?shareId=xxx&authorId=xxx 用法: python qianwen_video_downloader.py <share_url> [-o output.mp4] [--all] [--info] """ import re import json import sys import argparse from pathlib import Path from urllib.parse import urlparse, parse_qs, unquote try: import requests except ImportError: print("📦 正在安装 requests...") import subprocess subprocess.check_call([sys.executable, "-m", "pip", "install", "requests", "-q"]) import requests # ── 常量 ───────────────────────────────────────────────────────── HEADERS = { "User-Agent": ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" ), "Referer": "https://www.qianwen.com/", "Content-Type": "application/json", } # 允许代理下载的 CDN 域名白名单 ALLOWED_CDN_DOMAINS = [ "workspace-zb-cdn.quark.cn", "quark-aistudio-cdn.quark.cn", "img.alicdn.com", ] # ── URL 识别 ──────────────────────────────────────────────────── def detect_share_type(url: str) -> dict: """ 识别分享链接类型,返回: {"type": "qianwen_chat", "share_id": "xxx"} 或 {"type": "activity_share", "share_id": "xxx", "author_id": "xxx"} """ parsed = urlparse(url) # 类型1: www.qianwen.com/share/chat/{id} m = re.search(r'/share/chat/([a-f0-9]+)', url) if m: return {"type": "qianwen_chat", "share_id": m.group(1)} # 类型2: activity.qianwen.com/...?shareId=xxx&authorId=xxx if "activity.qianwen.com" in parsed.hostname: qs = parse_qs(parsed.query) share_id = qs.get("shareId", [None])[0] author_id = qs.get("authorId", [None])[0] if share_id: return { "type": "activity_share", "share_id": share_id, "author_id": author_id or "", } raise ValueError(f"无法识别的链接格式: {url}") # ── API 调用 ──────────────────────────────────────────────────── def fetch_type1(share_id: str) -> dict: """类型1: chat2-api.qianwen.com""" print(f"📡 类型1 API (chat2-api) share_id={share_id[:16]}...") resp = requests.post( "https://chat2-api.qianwen.com/api/v1/share/info", headers=HEADERS, json={"share_id": share_id, "biz_id": "ai_qwen"}, timeout=30, ) resp.raise_for_status() data = resp.json() if data.get("code") != 0: raise ValueError(f"API 错误: {data.get('msg')}") return parse_type1_data(data["data"]) def fetch_type2(share_id: str, author_id: str) -> dict: """类型2: qwen-api.zaodian.com""" print(f"📡 类型2 API (zaodian) share_id={share_id[:16]}...") resp = requests.post( "https://qwen-api.zaodian.com/api/v1/share/get", headers={**HEADERS, "Referer": "https://activity.qianwen.com/"}, json={ "shareId": share_id, "authorId": author_id, "chid": "null", "product": "ai_studio", }, timeout=30, ) resp.raise_for_status() data = resp.json() if data.get("code") != 0: raise ValueError(f"API 错误: {data.get('msg')}") return parse_type2_data(data["data"]) # ── 数据解析 ──────────────────────────────────────────────────── def parse_type1_data(api_data: dict) -> dict: """解析类型1 API 响应 (chat2-api)""" title = api_data.get("title", "未知") records = api_data.get("session", {}).get("record_list", []) for record in records: for msg in record.get("response_messages", []): meta = msg.get("meta_data", {}) for item in meta.get("multi_load", []): if item.get("type") != "ai_generate_video": continue content = item.get("content", {}) resource_infos = content.get("resource_infos", []) layout_list = content.get("layout_list", []) layout = layout_list[0] if layout_list else {} display_refs = layout.get("video", []) download_refs = layout.get("download_video", []) cover_refs = layout.get("cover", []) resources = {r["refer_id"]: r for r in resource_infos} extra = content.get("extra_info", {}).get("content", {}).get("extra", {}) all_res = [] # video = 无水印 for ref_id in display_refs: if ref_id in resources and ".mp4" in resources[ref_id].get("url", ""): all_res.append({**resources[ref_id], "label": "无水印", "type": "video"}) # download_video = 有水印 for ref_id in download_refs: if ref_id in resources and ".mp4" in resources[ref_id].get("url", ""): all_res.append({**resources[ref_id], "label": "有水印", "type": "video"}) # 封面 for ref_id in cover_refs: if ref_id in resources: all_res.append({**resources[ref_id], "label": "封面", "type": "image"}) # 其他 known = set(display_refs + download_refs + cover_refs) for ref_id, res in resources.items(): if ref_id not in known: ext = "原图" if ".png" in res.get("url", "") else "其他" all_res.append({**res, "label": ext, "type": "image"}) return { "source": "qianwen_chat", "title": title, "model": extra.get("model_name", content.get("generate_model_name", "")), "scene": content.get("scene", ""), "duration": content.get("duration", 0), "resolution": content.get("ratio", ""), "prompt": extra.get("query", content.get("prompt", "")), "allResources": all_res, } raise ValueError("未找到视频资源") def parse_type2_data(data: dict) -> dict: """解析类型2 API 响应 (zaodian)""" title = data.get("title", data.get("shareTitle", "未知")) prompt = data.get("content", {}).get("prompt", "") scene = data.get("objectType", "") all_res = [] play_info = data.get("playInfo") or {} image_info = data.get("image") or {} # 视频资源 if play_info.get("url"): all_res.append({ "refer_id": "video_main", "url": play_info["url"], "label": "视频(主)", "type": "video", }) if play_info.get("playUrl"): all_res.append({ "refer_id": "video_play", "url": play_info["playUrl"], "label": "视频(播放)", "type": "video", }) if play_info.get("downloadUrl"): all_res.append({ "refer_id": "video_download", "url": play_info["downloadUrl"], "label": "视频(下载)", "type": "video", }) # 封面图 if image_info.get("url"): all_res.append({ "refer_id": "cover", "url": image_info["url"], "width": image_info.get("width"), "height": image_info.get("height"), "label": "封面", "type": "image", }) return { "source": "activity_share", "title": title, "model": "", "scene": scene, "duration": play_info.get("videoTotalTime") or 0, "resolution": "", "prompt": prompt, "allResources": all_res, } # ── 下载 ──────────────────────────────────────────────────────── def download_file(url: str, output_path: str, desc: str = "文件") -> str: """下载文件,带进度条""" print(f"⬇️ 正在下载{desc}...") resp = requests.get(url, headers={"User-Agent": HEADERS["User-Agent"]}, stream=True, timeout=120) resp.raise_for_status() total = int(resp.headers.get("content-length", 0)) downloaded = 0 with open(output_path, "wb") as f: for chunk in resp.iter_content(chunk_size=8192): f.write(chunk) downloaded += len(chunk) if total > 0: pct = downloaded / total * 100 filled = int(pct // 2) bar = "█" * filled + "░" * (50 - filled) print(f"\r [{bar}] {pct:.1f}% ({downloaded:,}/{total:,} bytes)", end="", flush=True) print() size_mb = Path(output_path).stat().st_size / (1024 * 1024) print(f"✅ 已保存: {output_path} ({size_mb:.2f} MB)") return output_path def generate_filename(info: dict, suffix: str = "", is_video: bool = True) -> str: parts = ["qianwen"] if info.get("model"): parts.append(info["model"].replace(" ", "_").replace("/", "_")) if suffix: parts.append(suffix) ext = "mp4" if is_video else "jpg" return "_".join(parts) + f".{ext}" def print_info(info: dict): print(f"\n{'='*60}") print(f"🎬 视频信息 [{info['source']}]") print(f"{'='*60}") print(f" 标题: {info['title']}") if info.get("model"): print(f" 模型: {info['model']}") if info.get("scene"): print(f" 场景: {info['scene']}") if info.get("duration"): print(f" 时长: {info['duration']}秒") if info.get("resolution"): print(f" 分辨率: {info['resolution']}") if info.get("prompt"): prompt = info["prompt"] if len(prompt) > 120: prompt = prompt[:120] + "..." print(f" 提示词: {prompt}") print(f"\n📦 资源列表:") for i, res in enumerate(info["allResources"]): w = res.get("width", "?") h = res.get("height", "?") url = res.get("url", "") ext = "MP4" if (res.get("type") == "video" or ".mp4" in url) else "PNG" if ".png" in url else "JPG" size_str = f"{w}×{h}" if w != "?" else "" tag_class = {"无水印": "✅", "有水印": "⚠️", "封面": "🖼️", "原图": "🖼️", "视频(主)": "🎬", "视频(播放)": "▶️", "视频(下载)": "📥"}.get(res["label"], "📎") print(f" [{i}] {tag_class} {res['label']} · {ext} {size_str}") # ── 主流程 ─────────────────────────────────────────────────────── def main(): parser = argparse.ArgumentParser( description="通义千问 AI 视频无水印下载工具 (支持两种分享链接)", formatter_class=argparse.RawDescriptionHelpFormatter, ) parser.add_argument("url", help="千问分享链接 (支持 qianwen.com 和 activity.qianwen.com)") parser.add_argument("-o", "--output", help="输出文件名") parser.add_argument("--all", action="store_true", help="下载所有资源") parser.add_argument("--info", action="store_true", help="仅显示信息,不下载") parser.add_argument("-i", "--index", type=int, default=-1, help="下载指定索引的资源 (从 --info 列表中选)") parser.add_argument("--watermark", action="store_true", help="下载带水印版本 (类型1)") args = parser.parse_args() try: link_info = detect_share_type(args.url) if link_info["type"] == "qianwen_chat": info = fetch_type1(link_info["share_id"]) else: info = fetch_type2(link_info["share_id"], link_info["author_id"]) print_info(info) if args.info: print(f"\n(--info 模式,不下载)") return print(f"\n{'='*60}") all_res = info["allResources"] if args.all: for res in all_res: url = res.get("url", "") is_vid = res.get("type") == "video" or ".mp4" in url ext = "mp4" if is_vid else ("png" if ".png" in url else "jpg") fname = generate_filename(info, res["refer_id"], is_vid).replace(".mp4", f".{ext}") download_file(url, fname, f"{res['label']}") elif args.index >= 0: if args.index >= len(all_res): print(f"❌ 索引 {args.index} 超出范围 (共 {len(all_res)} 个)") sys.exit(1) res = all_res[args.index] fname = args.output or generate_filename(info, res["refer_id"], res.get("type") == "video") download_file(res["url"], fname, res["label"]) else: # 默认下载第一个无水印视频 target = None for res in all_res: if "无水印" in res["label"] or "下载" in res["label"]: target = res break if not target: target = all_res[0] if all_res else None if not target: print("❌ 没有可下载的资源") sys.exit(1) fname = args.output or generate_filename(info, is_video=target.get("type") == "video") download_file(target["url"], fname, target["label"]) print(f"\n🎉 完成!") except Exception as e: print(f"\n❌ 错误: {e}") sys.exit(1) if __name__ == "__main__": main() 1 个帖子 - 1 位参与者 阅读完整话题