01 文档基线、目标与技术边界
本文描述听文视频工作台Android正式版1.0.2与独立试用版1.0.2-trial的实际实现,覆盖产品到代码、输入到成片、任务到文件、构建到发布的完整链路。适用对象为开发人员、技术交付人员、维护人员以及需要判断产品实现范围的评审者。本文不是将来功能方案:已实现行为、测试证据和现有局限分开表述。
| 基线项 | 正式版 | 试用版 |
|---|---|---|
| APK versionName | 1.0.2 | 1.0.2-trial |
| versionCode | 10002 | 10002 |
| applicationId | cn.tingwen.workbench |
cn.tingwen.workbench.trial |
| Java namespace | cn.tingwen.workbench |
cn.tingwen.workbench |
| 导出provider authority | cn.tingwen.workbench.exports |
cn.tingwen.workbench.trial.exports |
| 保留的PC业务基线 | PC1.5.9 | PC1.5.9,附累计试用控制 |
| Android SDK | min24 / target34 / compile36 | 相同 |
| ABI | arm64-v8a、x86_64 | 相同 |
技术实现使用原生Java宿主与WebView承载交互、Chaquopy内嵌Python、Flask提供回环HTTP业务接口、Pillow加工画面、在线Edge TTS生成MP3、可选Seedream生成图片,以及打包的Android原生FFmpeg/FFprobe执行编码与探测。手机不依赖PC在线、不下载本机绘图模型,不存在远程Web应用托管层。
需要区分三条边界:界面在哪里显示(WebView)、业务在哪里执行(应用进程内Python)、哪些数据离开设备(联网TTS正文、用户选择的图片服务器访问、用户明确同意的Seedream提示词/认证及主动分享)。视频编码和照片规范化在手机本地,但不是所有输入处理都完全离线。
源码归档中仍保留PC兼容函数、Draw Things模块、CLI脚本和历史版本记录;这些不代表APK现行用户入口。特别是Android启动会替换视频worker,绕过桌面Python子进程合成分支。本文所有编码参数均取Android实际worker,而非保留CLI的可选字幕或macOS硬件编码分支。
编制日期:2026-10-10。正式版代码发布目标提交为7fa6bf848d5f6718a38838f56b5754e267cba19f;试用版发布目标为14f47dc5edadcd5d498a71ae039b1d44feaef3f1。后续主分支fb7e0c1补充发布核验记录,未改已签APK。原始源码及证据目录的SHA256索引附于本文最后,便于核对资料基线。
02 总体架构与部署拓扑
分层职责
| 层次 | 实现组件 | 职责 | 不承担的职责 |
|---|---|---|---|
| Android宿主层 | MainActivity、WorkbenchService、StartupAnimationView、ExportPolicy、ExportProvider | 生命周期、启动画面、WebView、系统选择器、通知、唤醒、另存与分享安全 | 不实现TTS模型、不承载远端账号授权 |
| 前端交互层 | Jinja模板中的内联JS、mobile.js、mobile.css | 文案和标题、音色、图片来源、状态展示、确认操作、草稿、四页导航 | 不作为唯一的验证或额度权威 |
| 本机服务层 | android_runtime.start、Flask app、guard | 手机私有回环服务、鉴权、JSON接口、资源和媒体返回 | 不面向局域网或互联网开放API |
| 业务调度层 | app.py、Control、WORKERS、ACTIVE、CONTROLS | 创建任务、线程调度、阶段状态、确认闸门、暂停/取消、恢复、素材保留 | 无持久化多任务队列、无OS JobScheduler |
| 素材处理层 | speech.py、photo_import.py、seedream.py、标题脚本 | 在线语音、输入图片安全、云端图像适配、画面绘制 | 无端侧大模型、无文字AI改写 |
| 媒体执行层 | Android FFmpeg/FFprobe PIE程序 | H.264/AAC编码、格式/时长/流探测、进度输出 | 无Android MediaCodec硬件编码分支 |
| 私有数据层 | JSON/JSONL、PNG、MP3、MP4 | 草稿、任务快照、语音片段、素材、成品、日志、试用账本 | 无SQLite/Room、无同步服务器 |
进程与线程
Activity、Service、WebView宿主和Chaquopy均处于默认应用进程,Manifest没有独立android:process声明。Service有单线程计划执行器,Python本机HTTP服务以daemon线程运行且请求处理支持线程;每个业务步骤由daemon线程执行,音频步骤内部运行asyncio事件循环。FFmpeg和FFprobe通过Python子进程执行,不由JNI媒体接口加载。
libffmpeg.so和libffprobe.so虽使用.so名字,实际是Android ELF64 PIE可执行程序。这个名称方便Android安装时将其提取到nativeLibraryDir;执行路径来自系统应用信息,不依赖PATH中另行安装FFmpeg。
Chaquopy作为Android Gradle插件将Python运行时、Python业务和对应ABI依赖打入APK;本项目使用Java API在宿主中启动与调用Python模块。Chaquopy 17 Gradle文档说明这一接入机制;具体版本和模块调用以本项目配置为准。
依据:app/build.gradle:9–55、AndroidManifest.xml:14–27、WorkbenchService.java:47–57、android_runtime.py:168–187、app.py:325–366。
03 模块、目录与接口责任
| 源码模块 | 核心责任 | 主要输入/输出 |
|---|---|---|
MainActivity.java |
主WebView、开场覆盖层、原生桥、文件选择、保存/分享、返回键 | 系统Intent/本机URL/导出路径 → 页面、URI、原生提示 |
WorkbenchService.java |
拷贝应用资源、启动Python/Flask、阶段通知、WakeLock | files根/native目录/随机密钥 → baseUrl与运行摘要 |
StartupAnimationView.java与StartupTiming.java |
Canvas科技蓝动画及首帧计时 | 首帧时刻、页面ready → 是否揭示WebView |
ExportPolicy.java |
同源和文件路径二次校验 | URL/文件 → 安全判定 |
ExportProvider.java |
临时内容URI映射、只读访问 | token/文件名 → MIME、描述、只读文件描述符 |
android_runtime.py |
Android启动、实际视频worker、系统图像解码、导出解析 | Java调用、任务/Control → 本机服务与手机成片 |
app.py |
路由、任务schema、调度、状态、图片确认、历史和恢复 | JSON请求 → 任务JSON/媒体/日志 |
speech.py |
音色表、正文分段、受控流、有限重试、片段复用与拼接 | 正文/音色/控制 → part文件/audio.mp3 |
photo_import.py |
Base64/图片验证、批次保护、PNG规范化、公网URL安全 | 手机图/直链 → PNG字节与脱敏元数据 |
seedream.py |
方舟图像单次请求、结果未知分类、错误脱敏 | 用户Key/模型/prompt → 单张PNG与模型元信息 |
image_sources.py |
来源注册与客户端选择 | 环境/provider → 可用来源和客户端 |
records.py |
版本说明、安全事件日志、分页 | 白名单字段 → events.jsonl/响应列表 |
make_homepage_bg.py |
标题规范化、计数、字体与海报布局 | 原图/主副标题 → 标题画面 |
compose_video.py |
保留PC CLI;Android仅复用时间轴函数 | Android调用build_image_sequence → ffconcat |
trial_limits.py(仅试用版) |
累计账本、文字差异、照片凭据、迁移、失效联动 | 新正文/新图片 → 持久usage及到期状态 |
界面主业务脚本并非独立app.js:它位于templates/index.html的内联script,随后加载static/mobile.js改写手机端布局、导航及PC按钮行为。技术维护时必须同时查看模板、移动JS和移动CSS,单看原模板会把“打开文件夹”等PC文案误认为Android实际动作。
应用随包资源包括字体、26段试听、许可证、模板和静态文件。用户任务、运行日志、草稿、API Key和签名私钥不应进入源码归档或APK资源。文档中的数据结构与示例使用合成内容,不来自实际用户任务。
04 启动、3秒动态开场与前台服务
启动序列
- Activity创建隐藏WebView与原生开场覆盖层;同时启动WorkbenchService。动画和业务初始化并行,不用固定sleep阻塞主线程。
- StartupAnimationView使用Canvas/Paint/Path和渐变绘制科技蓝背景、轨道、声波及“文字/声音/视频”阶段。设计坐标按屏幕映射,首个实际绘制帧记录
SystemClock.uptimeMillis()。 - StartupTiming的时长常量为3000ms。显示页面需同时满足首帧发生、已满3000ms和
pageReady=true;页面慢时保持加载状态,失败时显示错误与“重试启动”。 - Service立即建立低重要性通知渠道并进入前台。API34及以上使用
FOREGROUND_SERVICE_TYPE_SPECIAL_USE;Manifest附带用户发起本机语音和视频任务的用途描述。 - 后台执行器将
assets/workbench拷到getFilesDir()/workbench;模板和静态文件按前缀更新,其他已有文件通常保留。 - 启动Chaquopy Python;Java用SecureRandom生成32字节密钥(256bit),并转为64个十六进制字符的字符串保存在运行内存。
- 调用
android_runtime.start(root,nativeDirectory,secret):设置环境、导入业务、替换视频worker、恢复任务状态、建立随机端口回环服务。 - Activity用
X-Android-Key头载入首页。返回首页设置HttpOnly Cookie,让同源子资源继续通过Android密钥验证。 - 页面ready后等待开场满足计时条件,隐藏覆盖层。Android13及以上的通知权限请求延后至开场完成、页面ready且Activity恢复状态,避免遮住动画。
关键常量与生命周期
| 项目 | 实际值/机制 |
|---|---|
| 开场最低时长 | 3000ms,从首个绘制帧起计 |
| 服务Host/Port | 127.0.0.1,端口0由系统动态分配;不是固定8091 |
| 通知渠道/ID | workbench_tasks/1001 |
| 服务状态轮询 | 每2秒调用Python status_json |
| 处理期唤醒 | PARTIAL_WAKE_LOCK,单次最长10分钟;仍忙时后续轮询可重新获取,空闲释放 |
| 服务重建提示 | START_STICKY;不能据此保证进程永远存活 |
| Activity销毁 | 销毁页面与预览资源,不主动停止前台服务 |
copyAssets代码还检查previews/前缀,但真实试听位于assets/previews/,不能将其泛化为“升级一定覆盖所有已有试听”。本版本试听内容未调整;未来资源升级应按实际相对路径核验。
通知展示ACTIVE步骤汇总,例如“合成MP3/准备照片/合成MP4”;精确分段与编码进度属于任务页。唤醒锁仅影响部分CPU休眠,不能阻止所有厂商省电清理、强行停止或存储故障。
依据:StartupTiming.java:4–12、StartupAnimationView.java:35–94、MainActivity.java:72–145、WorkbenchService.java:28–103、android_runtime.py:168–197。
05 本机HTTP鉴权、会话和WebView边界
请求验证顺序
| 层次 | 检查项 | 结果 |
|---|---|---|
| Android应用访问密钥 | X-Android-Key或tingwen_app Cookie与运行态secret恒时比较 |
不匹配403,拒绝进入业务 |
| Host | 仅127.0.0.1或localhost | 其他Host拒绝 |
| 来源 | POST和/api/session在提供Origin时检查其等于当前scheme/host/port;Sec-Fetch-Site不得为cross-site |
不同源拒绝;缺Origin本身不拒绝,无放开CORS |
| 本机会话 | 所有POST要求X-Local-Token等于本进程TOKEN |
缺失/过期403,带明确code |
| 内容类型 | POST必须JSON | 非JSON为415 |
| 请求体 | 最大64×1024×1024字节 | 超出413 |
| 业务校验 | 标题、音色、图片、状态、版本、额度等 | 不通过时400/409等 |
TOKEN通过secrets.token_urlsafe(32)生成,仅在运行内存;Android secret同样不写任务或草稿。首页Cookie设置HttpOnly与SameSite=Strict;回环HTTP没有设置Secure。首页和会话响应为no-store。HttpOnly能限制页面JS读取Cookie,不能替代主WebView导航控制和设备安全边界。
前端仅对服务器明确给出的local_session_expired拒绝进行一次会话恢复与重放:获取同源/api/session、校验产品名与token后重新发原请求。该拒绝发生在任务/模型处理之前,因此可以安全恢复。普通网络断连、Seedream超时或“响应未知”不属于可自动重发场景,防止产生第二次付费请求。
主WebView设置与导航
- 开启JavaScript和DOM storage,关闭file access;content access供系统选择器交互使用。
- 拒绝第三方Cookie、禁止mixed content、媒体播放要求用户手势;WebView摄像头/麦克风权限请求直接deny。
- 主页面仅允许当前
http://127.0.0.1:<port>同源导航。主机需字面127.0.0.1、无userInfo、合法且相同端口。 - 当前源资源、当前源拥有的blob、data和about:blank按代码规则放行;其他资源请求返回403。外部http/https/mailto/tel交给系统应用。
- 图片放大使用无AndroidNative桥的独立预览WebView,需用户手势;关闭预览会销毁其资源,避免把原生文件能力带给任意外部页面。
网络安全XML默认拒绝cleartext,仅回环域名放行。Python照片下载自行允许并验证公共HTTP/HTTPS,不能把XML设置误解为Python的网上图片只支持HTTPS。APK并未实现证书钉扎、设备加密数据仓库或跨设备身份服务。
依据:app.py:369–419、MainActivity.java:146–239、ExportPolicy.java:9–27、res/xml/network_security_config.xml。
06 前端交互、草稿与手机适配
前端通过四个pane组织创作、任务、作品和关于,mobile.js将PC定位文件行为适配为AndroidNative导出或作品页。首页大介绍区已去除,应用名下两行10px说明保留;360/390px检查避免标题区挤占文案输入空间。主标题placeholder“6G 通信”不等于默认写入标题。
| 交互机制 | 实现要点 |
|---|---|
| 正文统计 | Array.from(text).length,Unicode码点口径;长文输入框内部滚动 |
| 标题统计 | 规范化主副标题,去换行后合计;客户端提前拒绝30字符以上,服务端重复校验 |
| 音色 | 模板由VOICES渲染,所选按钮和摘要更新;样音来自本机preview接口,恢复不自动播放 |
| 本机照片预选 | FileReader读取、大小和数量预检、ObjectURL缩略图;重新选择替换批次,删除回收ObjectURL |
| 图片动作锁 | 防重复点击、换图期间禁用相冲突动作;服务端仍执行状态校验 |
| 任务轮询 | 选中任务约每2.5秒刷新,显示三个独立阶段状态 |
| 草稿 | input/change后300ms延迟,隐藏/返回时主动保存;只保存白名单字段 |
| 视频列表 | 请求实际磁盘成片列表,内嵌标准video控件;保存/分享交给原生 |
| 错误反馈 | alert/toast与步骤消息;结构化服务错误显示原因,模板文本以textContent插入数据 |
“清空”调用工作区清理逻辑,不向后端发送cancel,也不删除任务。它清输入、文件预选、Key、费用勾选、当前任务显示和本地清洗撤销缓存;正式版及未到期试用版保存空草稿。到期试用的flushDraft不再保存新草稿,清空正文还可能被计数锁回退为acceptedText,重启仍可恢复原已存正文。历史文件和活动工作仍由后端维护。用户想停止处理需要明确点击取消。
草稿恢复字段包括title,titleSecondary,text,prompt,voice,imageMode,provider,selectedId;不包含model、api_key、payment_consent、photo_url或File对象。模型不持久保存回草稿,已有任务的模型快照保存在job中。来源在Android草稿中固定为seedream。
正式版返回键保存行为与试用版不同:试用版额外等待串行计数/保存Promise后才触发原生finish,相关一致性细节见第17章。不得把试用专属修复扩写成正式版也具有同一队列实现。
依据:templates/index.html:154–282、static/mobile.js:4–101、static/mobile.css:3–19、app.py:427–465。
07 任务模型、调度与三阶段状态机
系统不是一个单独全局状态枚举。每个job有独立的audio_state、image_state、video_state,以及paused、cancelled、submitting等跨阶段标记。音频、图片成功状态为ready;视频成功为done。queued/working用于活动判定,failed/cancelled/interrupted表示不同终止或恢复情形。
创建与工作器
新任务先规范化标题、验证正文/音色/配图方式、解码或校验照片与来源配置。在RLock内检查ACTIVE,若有活动任务则409拒绝创建;生成规范UUID、写job快照、创建Control,启动audio。手机上传图已在请求中处理并保存,所以无需另启动image线程;URL和Seedream会启动image,与音频工作器可并行。
ACTIVE是(job_id,part)集合,CONTROLS映射每任务共享控制对象。launch()避免同一步重复启动,用daemon线程执行WORKERS映射中的函数。Android启动覆盖WORKERS['video'],其他业务继承PC规则。全局RLock协调任务读写、活动集和动作,不形成跨进程数据库事务。
状态与动作关系
| 动作 | 状态改变/处理 | 活動限制 |
|---|---|---|
| create | audio queued,image queued或ready,video idle | ACTIVE非空拒绝新任务 |
| pause | 设置paused事件及持久标志 | 对在途云图片不能撤回服务端处理 |
| resume | 清paused/cancelled,必要时新Control | 取消尚未完成则拒绝;不自动launch中断步骤 |
| cancel | cancelled事件,清前端暂停显示;未运行busy步骤设cancelled | 正在运行worker在检查点停止 |
| retry-audio | 重置audio queued,清素材确认,video idle | 同步/视频/导图或其他任务忙时拒绝 |
| retry-image | 重置image queued,沿用任务来源/模型 | manual图不使用这一AI重试入口 |
| regenerate | 更新prompt,重生成当前来源/模型图 | 新要求非空;Seedream重新认证/同意 |
| change-source | 使用新来源/模型配置并更新prompt | Android只有Seedream可选AI来源 |
| use-image | 保存手机图或下载直链,更新image_version | 允许同任务音频仍在处理,拒其他冲突步骤 |
| select-images | 原图库保留非空子集,新image_version | 有图/视频冲突步骤时拒绝,至少一张 |
| confirm | 保存当前confirmed_version/digest,video queued并launch | audio/image都ready,无暂停/取消/ACTIVE |
worker生命周期会记录开始、完成、失败或取消事件,finally清ACTIVE。异常消息替换当前Key,取消且所有相关步骤结束后清Control Key。没有retry-video独立action;视频失败或中断由confirm重新核对素材后启动。
图片存储、换图和删图都会清确认标识,使video回idle。旧MP4不立即删除,job_view单独查磁盘上保留成片并返回入口;再次成功合成以同名路径replace,属于一任务一个当前输出路径,并非永久多版本归档。
依据:app.py:35–58,163–195,325–366,590–643,667–839、android_runtime.py:181。
08 文案处理、标题校验与文件命名
正文与清洗
正式版不设产品级正文字符上限,仍受64MiB单请求、设备空间和外部TTS可用性约束。create以str(...).strip()得到实际朗读正文,拒绝空文本。试用版在strip前执行累计账本,故输入首尾空白可扣额但不朗读;这是两个不同处理目的。
前端显式清洗基于正则替换,统一换行、移除零宽/BOM、HTML注释和标签、Markdown链接地址/图片、裸网址、列表/引用/水平线、括号外壳及格式符,整理空白;保留数字、通常标点、括号内文本,数字间星号转换为“乘以”。不解码&等HTML实体,不调用语言模型、不识别文章事实,也不是通用Markdown语义解析器。最近一次有效清洗前原文用于撤销,手动编辑后撤销失效。
MD导入是另一套简化处理:删除完整三反引号代码块、标题前缀、图片、链接地址和部分格式符。显式清洗只去代码围栏而保留代码正文;需要文案一致性时不能把两个入口当成相同转换。
标题规则
normalize_title统一换行,逐行去首尾、连续空白合并、丢弃空行并保留有效手动换行。title_character_count合计主副标题的非换行Python字符,超过30即400拒绝,在启动语音/图像请求之前执行。标题颜色和布局见第12章。新任务保存title_style=poster,旧style仍有兼容路径。
文件命名与路径安全
新MP4文件stem取实际正文前30个Python字符,将/\:*?"<>|及控制字符替换下划线,再整理首尾空格/点;空stem回退听文视频。主标题不影响新MP4名。已有任务没有video_filename时继续用video.mp4,不为历史文件自动更名。
任务目录使用规范UUID,必须str(uuid.UUID(id)) == id。照片文件必须basename、以image-开头并以.png结尾;视频文件同样只能basename且.mp4,无反斜杠。路径保护与导出白名单相结合,避免用户输入变为目录穿越参数。
依据:templates/index.html:215–248、app.py:63–105,593–605、make_homepage_bg.py:36–50。
09 TTS:分段、流控制、重试与MP3发布
语音请求链路
speech.synthesize接收任务正文、固定voice、任务目录、Control及progress/validate回调。默认factory为edge_tts.Communicate,每段调用Communicate(segment,voice).stream()。应用处理流中type=audio的字节,不保存WordBoundary/SentenceBoundary作为字幕。
应用分段以2000个Python字符为上限,按空行拆段并去段落首尾;短段合并时计入两个换行,超长段落直接每2000字符硬切,未做语义句末对齐。每个片段按顺序处理,语音内部没有并发池;图片准备与整个音频步骤并行是另一层调度。
| 参数/机制 | 实际实现 | 含义 |
|---|---|---|
| 应用分段上限 | 2000字符 | 与UI累计试用10000字符不是同一个概念 |
| 单段尝试次数 | range(3) | 首次请求+最多2次自动重试 |
| 重试退避 | 1.5秒、3秒 | 在等待期间仍尊重暂停/取消 |
| 无流事件时限 | 30秒 | 每次流事件重置idle,含元数据事件 |
| 单次总流等待 | 120秒 | 暂停时间不累计;不是整个任务120秒 |
| 控制检查 | 约0.1秒的asyncio等待循环 | 网络等待期间也可观察取消 |
| 最终片段 | part-0000.mp3等 | 非空并probe成功后才rename |
| 临时片段 | part-0000.partial等 | 失败残片不成为成功分段 |
| 合并 | 按顺序直接连接MP3字节 | 没有重新编码、交叉淡化或自动补静音 |
已有part最终文件会跳过重请求,保证同一固定任务重试可复用完成段。合并逐块读取1MiB,检查Control,先写audio.pending.mp3,最终probe后replace成audio.mp3。最终ready状态保存audio_duration,后续图片时间轴使用这个实际媒体时长。
应用层与edge-tts库层
发布构建使用edge-tts7.2.3。库内部按字节限制切分SSML请求,并自动转义XML/生成voice与prosody;应用层2000字符段不保证只对应一条网络SSML。应用只传text与voice,不开放任意SSML、语速、音量或音调编辑。
构建库请求的音频格式为audio-24khz-48kbitrate-mono-mp3,上游WSS位于api.msedgeservices.com/tts/cognitiveservices。后续AAC192k/44.1k/stereo是容器制作参数,不能把源48kbit单声道语音升格为拥有更多原始信息的录音。多语言音色和语音质量最终取决于外部服务,源码无法证明其底层模型权重或全部语言行为。
本应用当前没有音频生成收费入口,不要求个人TTS API Key;edge-tts项目自身定位为使用Edge在线语音服务的Python接入。上游项目说明其联网方式;本文不据此承诺永久服务规则或访问权利。
媒体探测与进度
probe通过包内FFprobe,请求format duration及stream codec_type/width/height,30秒子进程时限;返回非零或duration≤0拒绝。它是有效媒体探测,不是每个采样/每帧完整解码扫描。音频百分比按completed/total段数计算,合并与验证没有确定百分比,网络消息包含等待秒数/接收KB。进度不承诺等比例剩余时间。
依据:speech.py:8–166、app.py:136–160及发布构建edge_tts源码快照。
10 图片导入、规范化与公网下载安全
手机照片契约与技术保护
| 保护项 | 值 | 检查阶段 |
|---|---|---|
| 每批数量 | 50张 | 前端及local_photos;试用额外10张累计 |
| 单图原始字节 | 20MiB | File读取预检/Base64解码与下载保护 |
| 批次原始字节 | 40MiB | 整批解码前累计验证 |
| 总像素 | 50,000,000 | 图片头/实际解码检查 |
| 单边 | 16384像素 | 图片尺寸验证 |
| 请求体 | 64MiB | Flask MAX_CONTENT_LENGTH |
上传以JSON Base64字符串传入:前端先从FileReader的Data URL结果剥除data:image/...;base64,前缀,后端只接受严格Base64,不会自动替API调用者剥除此前缀。不能只因扩展名就把任意数据当成图片。图片解码先校正EXIF方向、取第一帧、透明图铺白底、转换RGB并重编码PNG,删除原始元数据。整批先验证,某张失败提示对应位置,不部分保存并替换已有任务。
常见JPEG/PNG/WebP/GIF/BMP/TIFF由Pillow读取。遇到ISO BMFF的ftyp类数据且Pillow失败时,Android路径尝试系统ImageDecoder,再把Bitmap压成PNG重新规范化,检查像素并recycle。ImageDecoder是API28能力,minSdk24设备未必具有该类或相应格式解码,异常会给出读取失败;APK没有强制打包pillow-heif解码器。
输入大小限制是压缩原始字节,不限制转换后所有PNG总大小或精确解码峰值。规范化源图保留像素,直到cover阶段才缩成1080p;高像素PNG、许多照片和长任务仍可能增加内存/空间压力。
公网URL校验与连接
URL必须http/https、host非空、无user/password、无空白控制字符,长度最多8192,端口仅80或443,拒绝host中的zone编码。DNS结果中的每个IP都需公网global且非multicast/reserved,拒绝私网、回环、IPv4映射绕过、6to4、Teredo和NAT64特殊前缀;全部安全后优先IPv4。
下载将连接绑定到已验证IP;HTTPS仍以原域名做SNI和证书校验,减少DNS重绑定风险。每次跳转重新验证,最多4次请求即最多3个有效跳转,拒绝HTTPS降级HTTP。socket等待20秒不等于整个下载总时长保证20秒;代码没有另一个全过程总计时器。
响应要求200、非明显HTML,大小最多20MiB。请求不携带用户Cookie、云Key或账号,Accept-Encoding=identity;兼容gzip/deflate时还限制解压大小,拒绝异常或未支持编码。下载结束仅保存规范化图片与最终来源域名元数据,不持久保存完整URL。
网址任务的URL只在Control内存中,处理finally清空;重启后的失败网址任务无法从job恢复原直链,需要用户重新提供。图片直链能力不含文章解析、登录绕过、网页多图提取或后台爬取。
依据:photo_import.py:17–192、android_runtime.py:272–289、app.py:198–209。
11 Seedream云端图片适配与费用保护
Android环境在SOURCES中移除Draw Things,AI来源仅Seedream;manual代表用户照片/网址,不是另一AI供应商。没有在某个来源失败后自动切换收费云端的逻辑。
固定配置
{
"endpoint": "https://ark.cn-beijing.volces.com/api/v3/images/generations",
"models": [
"doubao-seedream-5-0-pro-260628",
"doubao-seedream-5-0-flash-260915"
],
"request": {
"model": "doubao-seedream-5-0-pro-260628",
"prompt": "用户填写的图片要求",
"size": "2K",
"response_format": "b64_json",
"output_format": "png",
"watermark": true
}
}
这些模型ID和参数是APK固定配置,不表示本次文档编制执行过真实付费调用或确认所有账户都已开通。当前界面不提供size、水印、输出格式或数量的可调开关。
请求必须具备有效Key和payment_consent is True。Key可由JSON输入或运行环境ARK_API_KEY提供,代码拒绝空、换行或超过2048字符;APK不内置预设Key。HTTP请求使用Authorization Bearer,单次timeout180秒,读取响应最多48MiB。解析要求恰好一张图片、Base64文本≤44MiB、宽高至少64且像素≤1600万,重编码为RGB PNG,不依赖不可信临时下载URL。
已知失败与结果未知
| 分类 | 示例 | 应用处理 |
|---|---|---|
| 明确参数/权限/额度失败 | 400、401、403、404、部分429及官方code | 转换中文原因,隐藏Key/Bearer/远程URL;不自动改账户限额 |
| 余额/模型状态 | InsufficientBalance、Arrearage、SetLimitExceeded、NotOpen等 | 按官方code区分,要求用户处理控制台 |
| SubmissionUnknown | HTTP5xx、连接/超时、JSON异常、响应过大或不可用单图 | 保留可能已计费的未知状态,禁止自动重发 |
提交前保存submitting=true,返回正常图片后保存素材。用户在云请求途中取消时,已经提交的请求不能由手机撤回;若图片返回,仍尽量落盘,不继续视频制作。手动重试已知结果未知的Seedream任务要求confirm_resubmit,同时重新验证Key及费用同意。切换为手机照片不能证明之前云请求没有计费,风险标识继续保留。
Key仅在Control与运行内存中,不写job、草稿或日志;错误消息替换当前Key,取消且步骤结束清Key。代码没有安全零化所有内存副本的机制,故“只在内存”应理解为未持久写入,而不是绝对没有任何内存观察风险。
依据:seedream.py:11–94、app.py:501–528,216–225,777–804、image_sources.py:6–19。
12 1080p画面、字体与主副标题布局
画面算法
Pillow在应用Python进程内打开规范化图片,EXIF转正并RGB化。背景直接resize至1920×1080,GaussianBlur(60)形成铺满画面;前景按min(1920/source_width,1080/source_height)等比缩放并ceil像素,居中贴到背景。前景保持整张图可见,背景可因拉伸而改变比例,但只承担模糊填充。
每张照片生成一个cover文件:首张cover.png携带任务title/title_secondary,后续cover-1.png等不加标题。任务图像预览为原素材,不在预览接口提前写入海报标题;最终标题需在生成的视频中查看。
海报标题参数
| 项目 | 当前poster路径 |
|---|---|
| 字体优先级 | 随包NotoSansCJKsc-Bold.otf,随后系统/兼容回退 |
| 深色底板 | #031127,画面x32%~68%,满高,宽度36% |
| 文本安全区 | x33.5%~66.5%,y13%~79% |
| 主标题自动分行 | 每行最多6字符,保留有效手动分行 |
| 副标题自动分行 | 每行最多7字符,副标题全行统一字号 |
| 主标题颜色 | 所有非换行字符前ceil(n/2)红#E1261C,余下黄#F7DE00;跨行连续统计 |
| 副标题颜色 | 全黄#F7DE00 |
| 主字号上界 | 画高×21%,实际按字形边界拟合 |
| 副字号上界 | min(画高×11.5%,最小主字号×0.9) |
| 总高度调整 | 超限时按0.96比例缩放直到符合区域 |
| 字形效果 | 水平压缩0.88,同色实心加粗,无黑色描边 |
标题完全留空时不加底板/文字。新任务固定poster;历史非poster任务可走legacy中央三色路径,用于兼容旧数据,不是当前提供的样式切换。某些PC历史文档提及macOS W6字体,不应作为手机默认字体说明。
源码提供基于字形像素边界的布局验证;当前实现没有提供用户手动拖拽标题、改色、改字号、调整底板位置的控件。维护时需保证前端规范化与后端30字符计数保持相同含义。
依据:android_runtime.py:37–55,113–125、make_homepage_bg.py:19–158、app.py:595–598,627。
13 时间轴、FFmpeg命令与成片验证
图片时间轴
设实际audio_duration为D,当前保留照片数为N。每张分配D/N,以原图库顺序写ffconcat,duration写九位小数;最后重复最后一个文件条目,让其duration生效。它不多加一个独立照片段。多图切换按5fps帧时间量化,不能描述成逐音频采样精确对齐;也不根据文案语义或图像内容智能配时。
ffconcat version 1.0
file '/私有任务目录/cover.png'
duration 30.000000000
file '/私有任务目录/cover-1.png'
duration 30.000000000
file '/私有任务目录/cover-2.png'
duration 30.000000000
file '/私有任务目录/cover-2.png'
以上是90秒、3张图的机制示例,不是实际用户文件。路径由任务内部生成,不直接接受用户自由拼接。
Android实际编码参数
<nativeLibraryDir>/libffmpeg.so
-y -progress <job>/encode-progress.txt -nostats
-f concat -safe 0 -i <job>/image_sequence.ffconcat
-i <job>/audio.mp3
-filter_complex <按单图或多图生成的滤镜>
-map [v] -map 1:a
-c:v libx264 -preset veryfast -crf 21 -threads 2
-c:a aac -b:a 192k -ar 44100 -ac 2
-r <单图1/多图5>
-shortest -t <D> -movflags +faststart
<job>/video.pending.mp4
单图:
[0:v]loop=loop=-1:size=1:start=0,trim=duration=D,
setpts=N/(1*TB)[base];[base]format=yuv420p[v]
多图:
[0:v]fps=5,tpad=stop_mode=clone:stop_duration=2,
trim=duration=D[base];[base]format=yuv420p[v]
单图loop重复首帧减少反复解码,多图tpad延续末帧避免-shortest提前截尾;-t限制到音频时长。libx264为软件编码,veryfast和两线程是当前资源取舍,不存在“调用Android硬件加速”的分支。faststart调整MP4索引布局以便播放;格式机制参考FFmpeg格式文档,本项目参数以代码为准。
编码控制和实际进度
subprocess.Popen(...,start_new_session=True)启动独立进程组,循环约0.15秒检查Control并调用进度更新。暂停发SIGSTOP,继续发SIGCONT。异常/取消收尾先恢复停止态,再SIGTERM,等待3秒仍未退出则SIGKILL,防止只停止父进程而遗留编码子进程。
FFmpeg向encode-progress.txt输出out_time_us;读取文件尾8192字节,求out_time_seconds/D×100,编码阶段cap99。stdout/stderr追加到video-log.txt,非零退出时取日志尾用于中文异常说明。当前未实现整个视频编码的独立总超时或无进度watchdog,不能把TTS的120秒套到长视频。
最终验证与原子发布
编码成功后再check_cancel,FFprobe核对存在audio stream、视频尺寸恰为1920×1080、format duration有效且与D差不超过1.5秒。通过后replace临时视频到任务video_filename,设video_state=done、percent=100、保存video_duration和各图duration数组。这个规格检查不能替代完整逐帧解码或人工审核内容;回归用例中的完整主机解码是另外的测试证据。
依据:android_runtime.py:58–165、app.py:228–238、compose_video.py:104–123。
14 素材确认、暂停取消和进程恢复时序
确认既检查状态,又固定当前素材版本:音频和图片ready、未暂停/取消、客户端image_version等于当前、无ACTIVE,计算当前图片按顺序连接字节的SHA256,保存confirmed_version与confirmed_digest。video若已queued/working/done,重复confirm直接返回状态,不启动第二编码。
worker开始再次核对version与digest。换图、删图、重生成均使旧确认失效。这个闸门避免客户端旧预览确认新文件,也避免确认后磁盘素材变化;它不是对图片内容是否合适的自动审核。
控制语义
- Control共享paused与cancelled线程事件,音频异步流与合并步骤在检查点等待/退出;本机画面循环和编码循环也检查取消。
- pause不撤销已经提交Seedream请求。返回已付费素材时尽量保存,后续成片仍停止。
- cancel令正在处理步骤响应取消,并将未运行但busy的步骤改cancelled;真正终止以worker退出和ACTIVE清除为准。
- resume只清暂停/取消标志;不会自动launch cancelled/interrupted步骤。用户分别选择重试音频、重新选图、图片重试或confirm恢复视频。
recover与重启
Python服务启动调用recover扫描job.json,原queued/working改interrupted并提示手动重试,paused清false;ready/done及实际文件保留,submitting等未知收费信息保留。不自动TTS、不自动重新编码、更不会因重启自动重发收费图。
已成功part可在音频重试复用。failed/manual URL需要用户重新提供URL,因为Control.photo_url只在内存;历史图片则可直接读取。进程死亡时ACTIVE与Control不持久保存,只能根据任务JSON把活动状态改为待恢复。
源码中的recover/jobs扫描未对每个损坏job.json分别捕获错误,坏JSON可能使恢复或历史列表失败;文档不能承诺所有磁盘损坏自动修复。前台服务START_STICKY也不等于自动恢复每个任务步骤。
依据:app.py:48–58,747–819,871–880、android_runtime.py:58–110、speech.py:46–89,118–166。
15 数据目录、schema与持久化一致性
根目录由AndroidgetFilesDir()/workbench获得。常见路径可示意为/data/user/0/<applicationId>/files/workbench,实际应以系统API返回为准,不能在APK写死某个用户或设备路径。
workbench/
├── templates/index.html
├── static/mobile.js、mobile.css
├── assets/fonts/NotoSansCJKsc-Bold.otf
├── assets/previews/<voice>.mp3 # 26段
├── licenses/
├── draft.json # 写入中draft.pending
├── trial-usage.json # 仅试用版;写入中trial-usage.pending
├── jobs/<规范UUID>/
│ ├── job.json # 写入中job.pending
│ ├── part-0000.mp3 ... # 写入中part-0000.partial
│ ├── audio.mp3 # 写入中audio.pending.mp3
│ ├── image-<version>.png
│ ├── image-<version>-<index>.png # 多图
│ ├── cover.png、cover-1.png ...
│ ├── image_sequence.ffconcat
│ ├── encode-progress.txt、video-log.txt
│ ├── <正文前30字符清理后>.mp4
│ └── video.pending.mp4
└── records/
├── events.jsonl
└── tingwen-records.json # 导出时生成
job核心字段字典
| 字段组 | 字段 | 说明 |
|---|---|---|
| 标识/时间 | id、created、updated | 规范UUID、创建/更新时间戳 |
| 原始创作快照 | title、title_secondary、title_style、text、voice | 创建任务后固定,编辑草稿不回写 |
| 当前配图要求 | prompt | 创建时保存;regenerate/change-source可更新任务配图要求,单纯编辑草稿不回写 |
| 来源配置 | provider、model、image_mode | manual为手机图/网址;AI当前Seedream;历史可保留兼容配置 |
| 输出名 | video_filename | 正文命名,历史可回退video.mp4 |
| 图片版本 | image_version、confirmed_version、confirmed_digest | 当前图与确认闸门 |
| 图库 | image_gallery、image_metadata | 多图entry含filename/metadata/source_id;单图gallery可为空 |
| 状态 | audio_state、image_state、video_state | 分别维护 |
| 消息/进度 | 各阶段_message、各阶段_progress | 进度含stage、percent或completed/total |
| 控制/收费状态 | paused、cancelled、submitting、remote_id | remote_id为兼容字段,不实现SeedreamID恢复轮询 |
| 媒体时长 | audio_duration、video_duration、image_durations | 成功后写,时间轴依据audio_duration |
job_view响应额外附gallery(index、metadata、preview_url)、output_folder、audio_path、video_path、video_url;这些是视图便利字段,不必一概理解为job落盘schema。video_path来自独立实际文件检查,所以状态重置后仍可访问上次成片。
原子写入与局限
| 文件 | 写入机制 | 一致性边界 |
|---|---|---|
| job.json | RLock内写job.pending,replace正式文件 | 原子替换,未见job写入fsync;不构成数据库事务 |
| draft.json | pending、flush、fsync文件、replace | 字段白名单;与试用账本不是跨文件事务 |
| 试用ledger | pending、flush、fsync、replace、fsync目录 | 进程内RLock串行;非跨进程文件锁 |
| 图像/最终音频/视频 | 先临时后replace | 只有达到对应验证条件才发布为完成 |
| events.jsonl | 锁内追加安全字段 | 日志坏行可跳过,写失败标warning而不阻断媒体任务 |
原子rename和fsync改善持久化,不代表所有断电、介质损坏或空间耗尽都绝不会丢数据。任务文件未加密,必要正文与提示词保存在应用私有JSON;allowBackup=false关闭自动Android备份。没有用户级项目打包恢复、容量轮转或自动清理机制。
日志schema
白名单为event/job_id/import_id/call_id/part/status/provider/model/duration_ms/count,再加time/version。model字段进行安全格式检查;不接受任意error文本,避免把密钥、网址或完整内容当日志写入。读取逐行忽略坏行、时间倒序、默认分页100条。一次调用started/completed是生命周期两条记录,不代表两次云端收费,应用日志也不替代云端账单。
依据:app.py:63–129,427–498,626–659、records.py:75–113、试用trial_limits.py:61–197。
16 原生桥、系统保存与只读分享
NativeBridge接口
| 方法 | 正式版用途 | 试用附加情况 |
|---|---|---|
| saveDraft(json) | Python persist_draft,boolean返回 | 仍受累计计数,不能绕过额度 |
| exportUrl(url,share) | 将允许媒体URL解析成安全文件,保存或分享 | 到期也可导出已有成品 |
| exportPath(path) | 路径导出,先Python再Java检查 | 相同 |
| openLibrary() | 打开作品列表 | 相同 |
| appInfo() | 返回platform/sdk/真实APK version | 返回1.0.2-trial |
| trialUpgradeShown() | 不提供 | 读取首次成片购买提示标记 |
| showTrialUpgrade(completed) | 不提供 | true首次成片提示,false实际到期 |
| copyWechat() | 不提供 | 复制wzqy2019 |
| finishAfterDraft() | 不提供这一试用专属配合方法 | 保存队列成功完成后才后退/finish |
桥没有任意shell执行入口,其安全依赖主WebView只加载可信本机内容,并在导出时再次限制来源和路径。不能仅凭桥方法的@JavascriptInterface就认为任何远端页面都可获得相同能力。
多层导出校验
- URL必须当前本机源,只识别日志导出、任务媒体、图库图和已归档MP4的固定路由结构。
- Python checked_export解析文件路径,只允许root/jobs或root/records内现有非空文件,拒绝链接、未完成文件和其他扩展。
- resolve_export_path再检查任务三级路径:audio.mp3必须ready,MP4经archive_video_path,图必须当前image_paths或允许cover;records只能tingwen-records.json。不能通过桥导出job.json、任意私有文件或partial语音片段。
- Java ExportPolicy再次canonical路径验证,确认目录范围、类型和非空。
- 保存发ACTION_CREATE_DOCUMENT,按64KiB块向用户URI复制;取消选择不删除原文件,保存中保留pendingExport状态。
- 分享生成随机token,私有SharedPreferences保存安全路径映射,发content URI并授予只读权限/ClipData;Provider每次访问复核路径,只允许r模式,无增删改。
分享token映射代码中没有固定时间过期或回收器,故不能承诺“数分钟后自动失效”;系统URI读取授权与文件仍存在/合法共同决定能否读取。
Android系统文件选择和文档创建属于SAF机制;Android SAF文档解释通过系统选择器访问用户选定文件。项目实际导入还会按API版本优先系统照片选择器,且无需Manifest中的全盘存储权限。
归档视频接口和任务media/video条件不同:任务media/video只在done可用,/api/videos/<id>/media?filename=...按实际成片文件校验,可以在换图后访问保留旧MP4。兼容reveal接口在Android返回android_export_path或android_library,再由移动JS转换为保存/作品页,而不是启动Finder。
依据:MainActivity.java:277–360、ExportPolicy.java:29–47、ExportProvider.java:15–59、android_runtime.py:23–34,207–269、app.py:530–581,820–861。
17 累计试用:算法、持久化和到期联动
额度定义与饱和边界
全安装共用TrialUsage实例,text上限10000个Unicode码点、photos上限10。expired为持久停止标志,text_used≥10000或photos_used≥10任一即可触发。恰好到边界的计数/选图请求可HTTP200返回expired=true,随后制作入口全部关闭;超过余额的请求HTTP400并将计数饱和至上限。没有“第10张还可再生成一次”的宽限。
到期回调设置所有Control.cancelled并清paused,每个worker入口/关键check_cancel和launch也检查试用。编码循环约0.15秒观察并终止进程组;不能撤回已提交远端收费请求。已有成品/素材仍可读取、保存、分享,pause/cancel/reveal等保留动作不是重新制作。
新增文字算法
added_characters(previous, current):
相同字符串或current为空 → 0
current是previous子序列(纯删除) → 0
previous是current子序列(纯插入) → len(current)-len(previous)
否则去公共前后缀
SequenceMatcher(autojunk=False)
对insert/replace累计当前一侧字符数量
累计已用 = min(10000, 之前已用 + 新增数量)
删除/清空不减少已用,snapshot更新至已接受的当前正文
这是一套内容差异口径,不识别人的移动/替换编辑意图。相同草稿、相同提交不重复扣;删后恢复为新增,清洗只删除时不加不退,撤销恢复可能加额。只有朗读text计数,title/prompt不计本项。Unicode码点不是UTF-16长度或视觉字形;组合emoji可包含多个码点。
正常短MD先做导入格式处理再对处理正文计数;原始文件文本超过10000码点会先发超限计数令到期。input队列逐次计数而非防抖丢掉编辑动作;生成先等textQueue。IME composition期间跳过预编辑计数,compositionend计最终提交内容。恢复草稿先锁控件并初始化accepted snapshot,不重复扣旧草稿。
照片选择凭据
有效手机照片批次经读取/完整图像校验后消费新选张数,返回32位随机hex的photo_receipt。ledger将receipt映射到每项原Base64字符串SHA256列表,不含文件名,不是感知图像哈希。创建/换图带receipt时先检查仍active,然后按有序子序列匹配内容;原批或删减后原顺序子集重复提交不加额,乱序、伪造、增加原批没有内容或超重数重复被拒绝。
不带receipt的兼容直接上传重新消费;未知receipt不能偷偷退化为新扣一次。重新从系统选同图是新选择,删除预选/取消不退款。常规≤10张批次先完整校验再扣,失败不部分替换;单批>10是例外,在解码前按试用超额令到期。
URL/AI新图及AI图重试/重生成/切来源计1,在实际下载/配置/模型调用前完成;因此通过基本校验的新尝试即使远端失败或配置失败也可能已经消耗。图片重试/重生成/切来源及换图的前置在途冲突拒绝,以及空重生成要求、缺少未知收费重发明确同意等前置拒绝,不新扣。新建/api/jobs不同:文字/照片计数发生在ACTIVE检查之前,最终返回409仍可能已扣额,不能泛称所有失败或冲突都不消费。文字不增加照片额度,音频重试、现有图库筛选、confirm不新扣图。
账本结构与写入
{
"version": 1,
"text_used": 1500,
"photos_used": 2,
"text_snapshot": "此处为上次已接受正文;示例并非真实用户数据",
"receipts": {
"<32位随机hex凭据>": ["<每张Base64文本的64位SHA256>"]
},
"expired": false
}
可加migration_note说明保守迁移/异常原因。账本使用模块级RLock进行进程内读—改—写与凭据验证,pending写入、flush、fsync文件、replace、fsync父目录。计数写在草稿之前,两文件不组成原子事务;若账已写而草稿后续失败,可能已扣额而草稿未保存,代码没有退款机制。
损坏JSON、版本/schema错误、计数类型/范围错误、snapshot/expired/receipts不合法或凭据异常,采取满额+expired安全状态,保留坏文件,不当成新装空账。它不是加密防篡改账本、跨进程锁或远端授权;可阻止正常UI/接口并发漏账,不能宣称无法破解或卸载重置。
旧1.0.1迁移
无新账时,读取现存任务正文合计、最近任务正文与草稿新增差异;按每任务的现存图片数、图库/单图、创建/更换照片事件及图片模型调用次数进行保守照片估计,取必要最大值,裁到额度。缺任务而仍有创建事件等不确定情况fail closed。旧版没有记录每次历史编辑/删除,完全删除且不留证据的输入无法恢复,不能保证精确追溯。
前端锁与退出一致性
恢复中、额度未知、到期和退出pending均锁creationControls。updateUsage对迟到响应采用expired OR、used max、remaining min,使旧活跃响应不能重新解锁。换图的读取、选图计数和提交共用动作锁,避免任务切换和重复计数。
flushDraft先排队正文计数,再保存完整白名单草稿,返回本次saving Promise;尾部catch仅保持队列可用,不掩盖本次保存失败。真正返回退出时exitPending锁输入/清空,等待保存链成功结束后调用finishAfterDraft;失败则留在页面显示错误并恢复控制。到期时flushDraft返回已有保存链,不再执行新计数/草稿写入;等待链结束不等于到期后写入了一份新空草稿。IME未提交候选不作为草稿新内容。
完成联系提示与实际到期
trial-status的completed表示存在符合归档规则的已保存非空MP4,并非重新probe每个文件;completed与expired独立。首次成片联系提示经原生SharedPreferences记录“已实际显示”,跨随机服务端口保持一次性;真正到期优先显示并停止制作。关闭弹窗不解锁。联系微信wzqy2019;无支付、激活、续期或重置接口。
本机卸载/清数据删除账本与提示偏好;另一设备安装没有账号总额共享。正式试用独立包无自动任务迁移。原1.0.1正式版71文件隔离基线现在从Git标签android-v1.0.1核验,因为用户另行授权正式1.0.2更新,不代表当前正式源永远未变。
依据:试用trial_limits.py:10–271、app.py:49–96,469–507,603–647,680–929、static/mobile.js:36–135、试用MainActivity相应桥与退出代码。
18 全量HTTP接口目录与动作契约
以下目录由当前两版app.py AST提取装饰器生成,列出显式注册路由;Flask自动静态路由、HEAD/OPTIONS不作为业务入口另行展开。接口属于APK内部协议,不是对外公开云API;示例不包含真实密钥或运行token。
| 版本 | 方法 | 路径 | 作用/主要字段 | 源码定位 |
|---|---|---|---|---|
| 两版 | GET | / | 应用首页与同源Cookie | app/src/main/python/app.py:405 |
| 两版 | GET | /api/session | 恢复本机会话;不进入模型业务 | app/src/main/python/app.py:414 |
| 两版 | GET | /api/health | 产品版本与可用来源 | app/src/main/python/app.py:422 |
| 两版 | GET / POST | /api/mobile/draft | 读取/保存白名单草稿 | app/src/main/python/app.py:451 |
| 两版 | GET | /api/records | 版本及分页日志;kind、offset | app/src/main/python/app.py:467 |
| 两版 | GET | /api/records/export | JSON导出全部记录 | app/src/main/python/app.py:481 |
| 两版 | GET | /api/jobs | 历史任务摘要,创建时间倒序 | app/src/main/python/app.py:492 |
| 两版 | GET | /api/videos | 实际非空非pending成片列表 | app/src/main/python/app.py:542 |
| 两版 | GET | /api/videos/<job_id>/media | 按filename读取保留MP4;download=1另存 | app/src/main/python/app.py:560 |
| 两版 | POST | /api/videos/<job_id>/reveal | 兼容定位,在Android返回导出路径 | app/src/main/python/app.py:567 |
| 两版 | POST | /api/videos/reveal-root | Android作品库入口 | app/src/main/python/app.py:576 |
| 两版 | POST | /api/drawthings/check | 兼容路由;Android已移除该来源,拒绝配置 | app/src/main/python/app.py:584 |
| 两版 | POST | /api/jobs | 创建任务;正文/标题/音色/图片方式 | app/src/main/python/app.py:590 |
| 两版 | GET | /api/jobs/<job_id> | 完整job_view与三个阶段状态 | app/src/main/python/app.py:662 |
| 两版 | POST | /api/jobs/<job_id>/use-image | 换手机/URL照片,保留MP3并清确认 | app/src/main/python/app.py:667 |
| 两版 | POST | /api/jobs/<job_id>/select-images | 保留非空照片子集;版本检查 | app/src/main/python/app.py:709 |
| 两版 | GET | /api/jobs/<job_id>/images/<int:index> | 当前图库指定索引图片 | app/src/main/python/app.py:732 |
| 两版 | POST | /api/jobs/<job_id>/<action> | 动态动作:pause/resume/cancel/重试/确认等 | app/src/main/python/app.py:741 |
| 两版 | GET | /api/jobs/<job_id>/media/<kind> | 当前ready音频/图片或done视频 | app/src/main/python/app.py:847 |
| 两版 | GET | /api/preview/<voice> | 读取26种内置离线样音 | app/src/main/python/app.py:864 |
| 仅试用 | GET | /api/mobile/trial-status | 累计额度+是否有已保存成片 | app/src/main/python/app.py:603 |
| 仅试用 | POST | /api/mobile/trial/text | 正文差异计数并保存草稿正文 | app/src/main/python/app.py:616 |
| 仅试用 | POST | /api/mobile/trial/photos | 手机照片预选计数,返回内容绑定凭据 | app/src/main/python/app.py:633 |
动态action完整枚举
POST /api/jobs/<job_id>/<action>在服务端只接受下表动作;其他action为404,不能随意推断任意命令执行。请求必须JSON,暂无额外字段时也使用{}。
| action | 请求关键字段 | 成功行为/注意 |
|---|---|---|
| pause | 无 | 暂停标记,不撤云端提交 |
| resume | 无 | 清暂停/取消,不自动重启中断步骤 |
| cancel | 无 | 请求停止,素材保留 |
| retry-audio | 无 | 用任务快照复用已完成语音分段 |
| retry-image | api_key、payment_consent、必要时confirm_resubmit | 沿用任务AI模型,manual不提供AI重试 |
| regenerate | prompt及费用认证 | 更新要求,沿用任务模型重新生成 |
| change-source | provider、model、prompt、费用认证 | 从创作页配置更换可用AI来源/模型 |
| confirm | image_version | ready、版本一致、无ACTIVE后启动视频 |
| reveal | 可选kind=audio/video | Android返回导出路径;无kind打开作品库 |
共用GET媒体需应用密钥/Cookie,不需POST local token。POST统一令牌和来源保护,即使非收费动作也适用。无法直接在桌面浏览器随意使用APK随机端口接口,不存在无鉴权远程访问方式。
兼容路由的含义
/api/drawthings/check残留于共享业务代码,但Android SOURCES已移除drawthings,配置先拒绝来源,不可作为手机本机模型功能宣传。reveal类路由保留桌面分支,Android返回库/导出结构。remote_id字段兼容历史任务,Seedream当前直接b64返回单张,没有远端ID轮询恢复接口。
19 请求、响应、数据示例与错误码
创建本机照片任务
{
"title": "6G 通信",
"title_secondary": "产业协同与应用路径",
"text": "这是用于解释接口契约的合成朗读示例。",
"voice": "zh-CN-YunjianNeural",
"image_mode": "upload",
"photos": [
{"photo_name": "example.png", "photo_data": "<有效PNG字节的Base64,非data URI>"}
]
}
成功201返回job_view。试用版应先调用trial/photos取得photo_receipt并附加;已到期即使凭据有效也不能创建。photos数组为新多图契约,也兼容单图顶层photo_data/photo_name。所有图片在活动任务创建前校验,不因半批成功就启动任务。
URL/AI任务差异字段
{
"text": "用于演示URL任务的正文",
"image_mode": "url",
"photo_url": "https://example.org/example.png"
}
上面example.org是说明占位,不执行下载。URL模式不需prompt/Key。AI模式使用以下差异字段,Key必须真实由用户提供,不应写在文档、日志或源码中:
{
"text": "用于演示AI任务的正文",
"image_mode": "generate",
"provider": "seedream",
"model": "doubao-seedream-5-0-pro-260628",
"prompt": "横向构图的科技城市,蓝色光线,主体明确",
"api_key": "<由用户在运行时填写,不作为文档凭据>",
"payment_consent": true
}
素材筛选与确认
// POST /api/jobs/<id>/select-images
{"image_version": "<当前版本>", "selected_indices": [0, 2]}
// POST /api/jobs/<id>/confirm
{"image_version": "<筛选后的当前版本>"}
索引必须真实int、合法、非重复、非空;筛选仍按原图库顺序保存,不能当任意排序API。全选不改变版本;有变化会更新版本并清确认。confirm使用最新版本,不提交客户端自造摘要,SHA256由后端计算。
通用草稿字段
{
"title": "示例",
"titleSecondary": "副标题",
"text": "朗读正文",
"prompt": "图片要求",
"voice": "zh-CN-YunxiNeural",
"imageMode": "upload",
"provider": "seedream",
"selectedId": ""
}
注意草稿字段titleSecondary/imageMode为前端驼峰,任务title_secondary/image_mode为后端下划线。GET缺失/不可读草稿返回{},POST成功为{"ok":true};非法音色回默认、非法imageMode回upload、provider固定seedream、selectedId非法UUID回空。试用草稿保存会再次经过统一计数,不能绕过账本。
试用usage实际合成样例
下面样例来自独立临时目录中的Flask test_client调用,不包含个人内容,没有发起在线合成或收费请求。输入字符串“中😀,空格换行”用转义JSON表达实际5个码点,同文再次发送不增加。
// POST /api/mobile/trial/text
{"text": "中😀, \n"}
// HTTP 200
{
"expired": false,
"max_photos": 10,
"max_text_characters": 10000,
"photos_remaining": 10,
"photos_used": 0,
"text_remaining": 9995,
"text_used": 5,
"wechat": "wzqy2019"
}
trial-status额外有completed;trial/photos额外有photo_receipt;migration_note是可选说明。恰好10000字或10张HTTP200但expired=true,后续制作HTTP400。text非string如{"text":1}返回400“文案格式无效”;trial/photos仅upload模式,url不接受。
{
"error": "试用已到期:累计输入最多 10000 字(含标点、空格和换行)或累计选择 10 张照片;获取正式版请联系微信:wzqy2019"
}
HTTP结果分类
| 状态码 | 主要含义 | 处理建议 |
|---|---|---|
| 200 | 状态/读取/动作接受 | 仍需检查job状态或expired,不把200当制作一定成功 |
| 201 | 新任务已建立 | 继续轮询素材步骤 |
| 400 | 格式、参数、标题、图片或试用到期 | 显示error,按原因处理 |
| 403 | Android密钥、Host、Origin或本机会话不合法 | 仅明确过期拒绝可恢复一次会话 |
| 404 | 未知任务、动作、媒体或文件 | 不试图扩大路径范围 |
| 409 | 活动冲突、未就绪、素材版本变化、未知付费重发未确认 | 等处理结束或重新预览/明确决定 |
| 413 | 超64MiB请求体 | 压缩或减少照片 |
| 415 | 非JSON POST | 使用正确内容类型;可能为Flask默认HTML错误,不保证统一JSON |
| 500/其他异常 | 未被业务捕获的服务错误或环境故障 | 保留输入/素材,检查诊断,不自动重发收费动作 |
注册的400/403/404/409/413通常返回{"error":...};会话拒绝另有code。不是所有HTTP异常均有统一结构,文档/客户端应保留非JSON失败分支。
20 异常分类、可靠性与安全设计评估
| 场景 | 已实现保护 | 剩余边界 |
|---|---|---|
| 前端绕过限制 | 后端验证标题/音色/图片/状态;worker重核素材/额度 | 仍依赖可信安装和本机进程,非远程防破解授权 |
| 双击/同时操作 | 前端动作锁、ACTIVE、RLock、确认幂等 | 没有跨进程事务队列 |
| Seedream结果未知 | 提交标志、单次请求、明确人工重发同意 | 云端实际费用需用户查账户,不能本机撤回 |
| 残缺音频 | partial与最终part分开、非空probe、有限重试 | 已有part复用不逐个做完整采样验证 |
| 确认后换图 | 版本与内容digest双闸门 | 不审核图片含义、版权或适宜性 |
| 非法网络目标 | 全部DNS结果公网检查、IP绑定、跳转重校验 | 服务可用性/网站授权由外部决定 |
| 非法私有文件导出 | 同源、Python/Java目录类型复核、只读Provider | 无数据库加密或固定时间share token清理 |
| 进程中断 | 已完成素材保存、busy→interrupted、不重发收费 | 用户需手动恢复,损坏job JSON可能阻断恢复 |
| 磁盘写失败 | 临时替换、部分文件fsync、日志warning | 账本/草稿非跨文件事务;无自动数据修复 |
| 长任务/后台 | FGS、WakeLock、低帧率/两线程编码 | OS清理、温度、资源、无编码总watchdog |
应用自动日志不收正文/prompt/Key,任务JSON确实保存作品必要内容。业务控制令牌与密钥恒时比较、回环监听、主WebView同源、导出白名单共同形成边界;不应把某一个层次单独当成完整保护。
主机测试的恶意URL、目录穿越、令牌过期与费用路径检查说明相应业务分支被测试,但不等同全面渗透评估或所有Android机型上的系统安全验收。本章记录实现及明确限制,不引入未实现的“零风险”承诺。
21 工具链、依赖、原生兼容与许可证
实际打包依赖
| 组件 | 固定配置/版本 | 用途 |
|---|---|---|
| Android Gradle Plugin | 8.9.3 | APK编译打包 |
| Gradle | 8.13 | 构建执行 |
| Java | 编译/目标17;保留Zulu17.0.14工具链 | 原生宿主编译 |
| Chaquopy | 17.0.0 | CPython和Python依赖打包 |
| Python | 3.11系列 | 业务运行时 |
| Flask | 3.1.2 | 本机服务与模板 |
| Pillow | 11.0.0 | 图像解码、加工、字体 |
| edge-tts | 7.2.3 | 在线TTS协议接入 |
| aiohttp | 3.10.10 | 异步网络依赖 |
| FFmpeg | 7.1.2 | 编码/探测 |
| x264 | 保留stable精确快照,API165 | 软件H.264编码 |
| NDK | 28.2.13676358 | 两种Android原生ABI构建 |
| CMake | 3.22.1(Pillow兼容库构建) | FreeType/JPEG构建 |
| 字体 | NotoSansCJKsc-Bold.otf | 中文大标题 |
一级pip依赖用精确版本pin,但未发现传递依赖的完整hash lock,不能声称整个Python依赖图在任意新环境字节完全可重现。旧docs/PC回归测试.md写aiohttp3.9.5已陈旧;当前app/build.gradle和实际release staging均为3.10.10。主机测试虚拟环境的依赖版本可能更高,不应拿来填APK依赖表。
原生媒体构建
保留FFmpeg7.1.2与x264精确上游源归档及SHA256SUMS,不依赖会移动的stable分支重新取源码。NDK交叉编译默认API24、arm64-v8a/x86_64;ARM64使用NEON汇编,x86_64禁汇编以避NASM依赖。FFmpeg静态链接x264,Android libc/libm/libz等系统库动态链接。
构建启用GPL/libx264并用disable-everything配白名单:文件/pipe协议、concat/image2及必要音频/MP4解复用、PNG/JPEG/MP3/PCM/AAC/H.264解码、libx264/AAC编码、MP4/MOV封装、loop/tpad/fps/trim等滤镜。没有网络媒体解码protocol,也未启用Android硬件编码器;图片网络行为在Python执行。
16KB兼容:两个不同层次
- ELF层:各原生对象的LOAD段以0x4000对齐。FFmpeg/FFprobe可执行程序另核对PIE及Android64位动态链接器interpreter;Pillow依赖共享库核对SONAME、ABI、导入/导出符号与版本节点,不能要求共享库也具备可执行入口。
- APK ZIP层:用zipalign的
-P 16核对打包对齐;解包或zip对齐通过不能替代原生ELF和Python扩展的加载兼容。
FFmpeg/x264链接器传max-page-size/common-page-size=16384。Pillow原wheel部分FreeType/JPEG库较旧,项目保留对应API/SONAME的FreeType2.9.1与libjpeg-turbo1.5.3源码,NDK重建16KB兼容库,核对导出符号及Pillow所需符号。JPEG portable C关闭SIMD是兼容取舍,不能承诺性能等于原平台库。
Gradle跟踪native-compat输入,在installPythonRequirements任务完成后按ABI覆盖生成staging中的库;不手改pip生成目录后假装可重建。静态verify-binaries报告runtime_verified=false,说明不是设备加载测试。Android官方说明了含原生库应用的16KB适配背景:16KB页大小支持。
许可证和对应源
本FFmpeg构建因启用libx264/GPL按GPLv2或更高版本分发,x264也有相应GPL许可;项目保存许可证、精确源归档、构建脚本与来源SHA。Pillow依赖保留FreeType FTL及GPL替代说明、JPEG相关notice;Noto字体附OFL文本。APK资源同步包含需要的许可说明。
本节陈述打包组件的许可与保留资料,不能将其误解为完整法律审定。分发原生组件时,应保留对应源、通知和构建说明给接收方;不应只交一个无对应资料的媒体可执行程序。
依据:build.gradle、app/build.gradle、gradle-wrapper.properties、native-sources/README.md、licenses/README.md、native-compat/README.md、scripts/build-media.sh、scripts/build-pillow-libs.sh。
22 构建、签名、核验与GitHub发布流程
环境与输入
需要JDK17、Android SDK compile36、对应NDK、Python3.11、Gradle8.13。重建原生媒体还需Bash/make/tar/Perl,Pillow兼容库需要相应CMake。已有原生输出时可以只构建APK;全新环境的AGP/Gradle/Python依赖需缓存或仓库访问,只有保留原生源归档部分明确支持无网络重建,不能声称整APK在任意空环境离线构建。
APK release构建需要原签名文件和安全提供的TINGWEN_SIGNING_PASSWORD。脚本若缺原签名或密码会直接失败,不随意创建新密钥替换;该密钥未上传GitHub。调试构建使用调试签名,不可覆盖原正式发布。
可复核的构建步骤
# 从相应Android工程根目录执行,路径按实际环境提供
export ANDROID_SDK_ROOT=/path/to/android/sdk
export ANDROID_NDK_HOME=/path/to/android/ndk/28.2.13676358
export TINGWEN_NDK_ROOT="$ANDROID_NDK_HOME"
export TINGWEN_CMAKE_BIN="$ANDROID_SDK_ROOT/cmake/3.22.1/bin/cmake"
bash scripts/build-media.sh
bash scripts/build-pillow-libs.sh
python3 native-compat/verify_libraries.py
# 原签名及密码由维护人员安全恢复,本文不展示密钥值
bash scripts/build-apk.sh release
以上重建脚本按当前工程保留的macOS工具链路径编写;Linux/Windows不能仅改环境变量便假定原生重建可用,需要适配对应NDK host路径。build-pillow-libs读取TINGWEN_NDK_ROOT及ANDROID_SDK_ROOT/ANDROID_HOME,CMake可由TINGWEN_CMAKE_BIN指定。build-apk选择环境TINGWEN_JAVA_HOME或保留Zulu工具链,设置项目Gradle缓存;存在保留Gradle则使用,否则wrapper。release执行assembleRelease,debug执行assembleDebug及assembleDebugAndroidTest。产物默认在app/build/outputs/apk/release/app-release.apk;脚本本身不自动复制到dist、桌面或GitHub。
APK核验顺序
- aapt读取applicationId、versionName/code、应用标签与Manifest provider。
- apksigner验证实际签名,核对原证书公开指纹。
- zipalign
-c -P 16 4核对ZIP对齐;另执行原生ELF/符号检查。 - 解包比较45项资源与最终源文件、4个ABI媒体程序及Python归档业务;正式版检查无trial_limits,试用版检查额度常量与专属路由。
- 保存SHA256和实际字节数,再复制桌面、比对桌面与构建完全一致。
- 完成源/文档归档,排除jks、密码、Key、用户任务、草稿、ledger、日志及缓存;不重写原Git历史。
- 推main,Release指定已验证源码提交,上传ASCII文件名APK、SHA256SUMS与验证JSON。
- 用GitHubAPI核对Release非draft、目标提交、附件uploaded和服务端SHA256与桌面一致;正式版作为latest,试用版独立发布。
本次实际产物
| 版本 | 文件/Release下载 | 字节数 | SHA256 |
|---|---|---|---|
| 正式 | tingwen-android-v1.0.2.apk | 44,472,826 | aaa415bd54c1d48c3fd67099d12e37ca240ba4814fb60b7e2e2273182bd53b9a |
| 试用 | tingwen-android-trial-v1.0.2.apk | 44,492,954 | 0357aa2c91858797e2f87ef426a324b33fb00795c602d2295d7b9e1b5b3dbab1 |
公开证书SHA256为88184f3d44642ff29c1dba48018fa123ba8d2dcded94d83b6ad26e67a1a865f7,RSA3072,APK v2核验通过。该指纹不是私钥;它可帮助判断是否沿用原发布身份。同一应用覆盖升级须保持包名/密钥并提高versionCode。
项目GitHub为私有仓库;版本源码和附件链接需对应权限。本轮源归档不含用户作品或敏感运行资料,APK仅包含产品资源和代码。正式/试用独立包可并存;两者同code10002不相互覆盖。
23 验证证据、测试方法与验收边界
已交付1.0.2证据
| 维度 | 正式版1.0.2 | 试用版1.0.2 |
|---|---|---|
| 离线PC/Android业务回归 | 78项(70原PC快照+8安卓契约) | 76项(68原PC快照+8安卓契约) |
| Python专项主机检查 | 2项正式版API专项 | 45项接口/运行时/累计账本 |
| Chrome界面 | 3项,关于及360/390px紧凑布局 | 21项,含IME、恢复锁、迟到响应、换图锁、退出保存 |
| 合计 | 83项通过 | 142项通过 |
| Release内容/签名 | 原签名、45资源、4媒体程序、包/provider/ABI、ZIP16KB | 相应独立包、额度模块/路由、同类检查 |
| 新设备完整流程 | 本轮未追加 | 本轮未追加 |
试用回归因累计规则改变排除两个原PC精确界面/批次模拟断言,并用真实累计接口及Chrome检查覆盖新行为;原PC测试快照SHA未改,不以关闭后端额度来“让测试通过”。正式版专项验证反复保存12000字符以上草稿,12000字新任务配11和50张照片返回201,无trial ledger/路由、无云端配图调用。
主机媒体回归用Androidworker逻辑,但以桌面FFmpeg/FFprobe替代设备程序,验证单图/多图/标题/时长/音轨和完整解码;网络/付费路径使用mock,不产生真实Seedream费用。原生程序曾在Android14 ARM64模拟器执行版本命令,这与安装APK、WebView、系统选择器、真实TTS、分享和整条设备MP4验收不同。
后续设备验收建议(未执行项)
| 验收场景 | 判断标准 |
|---|---|
| 冷启动与失败恢复 | 首帧计时、pageReady条件、重试可用、通知不遮开场 |
| 实际联网语音 | 选定音色生成、全文结尾、超时/取消、重试复用 |
| 系统照片选择器 | 单选/多选、取消、系统上限、HEIC/AVIF按机型能力 |
| 完整本机视频 | 单图/多图1080p、标题、切图顺序、音轨、末尾播放 |
| 长任务与后台 | 锁屏、通知返回、系统清理、强停后待恢复,无收费自动重发 |
| SAF与分享 | 另存实际文件完整、取消保存保留源、接收应用只读读取 |
| 16KB设备 | Python扩展与两类原生程序实际加载,不仅ZIP检查 |
| 试用边界 | 9999→10000、9→10、任一耗尽在途编码停止、成品仍能导出 |
这些是维护验收方案,不是本次文档编制新跑过的检查。本文没有启动用户APK进行真机全流程,没有发起真实收费请求,也没有读取签名私钥。
测试执行入口
# 使用带所需依赖的主机Python虚拟环境,避免写入真实任务目录
python -B scripts/test-parity.py
python -B tests/test_formal_edition.py
# 浏览器检查需Playwright及可用Chromium/Chrome
node --test tests/test_about_ui.js
# 试用工程另有累计接口/UI及APK核验脚本
python -B scripts/verify-trial-apk.py
脚本依赖测试配置、主机FFmpeg、隔离临时根以及浏览器工具环境。不能只执行一个API单测便宣布设备编码、WebView和云服务全部通过。
24 维护、故障定位、性能和当前限制
定位顺序
- 启动层:查看Activity/Service的starting/ready/error、资源拷贝、Python初始化与baseUrl;不能先把3秒动画慢解释为TTS问题。
- 会话层:确认当前源、Android密钥Cookie和local token;仅明确过期拒绝可恢复一次。
- 任务层:核对保存快照、三阶段state、paused/cancelled、ACTIVE冲突及image_version。
- 语音层:看part完成数量、partial、等待消息和最后错误;分段重试遵守有限请求,不改旧任务正文。
- 图片层:区别decode/尺寸、URL公网与内容校验、云配置/未知结果;Seedream错误看官方code而不是只看429。
- 编码层:看cover、ffconcat、progress与video-log尾;确认尺寸/音轨/时长检查,pending不当成品。
- 导出层:确认文件已完成、路径白名单、系统URI选择和目标可写;分享目标支持MIME。
- 试用层:核对usage/migration_note、差异snapshot、receipt、是否已expired;不要自动删坏账重新发额度。
性能事实与容量
低静态帧率、veryfast和threads2减少编码负担是当前取舍,没有跨机型基准测试给出统一耗时。规范化高像素图片仍可能大,PNG背景/字体绘制在CPU上,软件编码耗时受时长、CPU与温度影响。语音依赖网络速度和服务响应,累计分段进度不代表剩余时间。
目前没有作业队列、自动媒体清理、容量水位控制、数据库压缩或日志轮转;每个任务保存原文/图片/分段/画面/成片,长时间使用会增加磁盘占用。设备磁盘满既可能影响任务保存,也可能影响试用账本/草稿,必须保留用户可读错误和现有素材。
未提供的功能与已知实现边界
- 没有离线TTS模型、IndexTTS/OmniVoice或Android系统TTS分支;没有克隆声音、任意SSML、语速音调UI。
- 没有字幕、ASR、按段落智能配图/配时、视频素材剪辑、背景音乐或转场编辑;保留CLI字幕选项不是APK功能。
- 没有PC本机Draw Things接入;保留检查路由在Android来源校验被拒绝。
- 没有账号登录、跨设备云同步、支付、激活/续期、设备永久试用绑定。
- 没有每次成片永久多版本快照;同任务最终路径可能被成功新成片替换。
- 无所有格式在API24均可读保证,无后台永不退出保证,无全编码总时限/无进度watchdog。
- 无整条数据的加密数据库,ledger是私有JSON;卸载/清数据会重置本机试用。
- 任务损坏JSON没有普遍逐文件恢复,依赖传递图未全hash锁定,share token映射无固定TTL清理。
Android前台服务类型和系统约束随版本变化,项目实际声明specialUse,不能因为存在前台通知而宣称通过所有应用商店政策审核或可无限后台运行。Android前台服务类型文档提供平台背景,实际交付仍需目标系统验收。
25 来源索引、术语与追溯资料
源码定位索引
下表由本次实际文件计算SHA256,并列代码行数。行号引用对应这一版本基线;后续源代码变化会使行号移动。用户任务、密钥、签名私钥和缓存没有进入索引。
| 版本 | 文件/代码位置 | 行数 | SHA256 |
|---|---|---|---|
| 正式 | app/build.gradle:1 | 80 | 9947b673daa3af8f383577f1b6de6399994dd2ae33c01a6a8b8450621666e354 |
| 正式 | build.gradle:1 | 4 | 09a91ee8bb9da6698ebcfc5a6550ba6b20821d20a62a0f7be83f6a70cc2bf2b4 |
| 正式 | app/src/main/AndroidManifest.xml:1 | 28 | f3845ce72f2830e2af677b346c018d8c69e42a5affd3eafdc4f2d61778315154 |
| 正式 | app/src/main/python/image_sources.py:1 | 19 | 52760db3913d01607ecc13a09323a29ec8a827544e0057fff57ef9af37172127 |
| 正式 | app/src/main/python/records.py:1 | 113 | 32c51c994243d4e75a3fada5f04c2c6a28df514a4fa7e4e11b43fbfb09004a4e |
| 正式 | app/src/main/python/android_runtime.py:1 | 289 | 5c71c5cee50ac78bf52d38c2256ffc47b9c47daa0ce608832cf84b14a22d2707 |
| 正式 | app/src/main/python/seedream.py:1 | 94 | a8f240662d6aa0206c0c428bc94ab1a6a9aa0d558e2610466c713de0f26ec78b |
| 正式 | app/src/main/python/app.py:1 | 889 | 90b8acf478c99375ef0a02945b456517074f874bf97f030bb6a7c39f7ff1485b |
| 正式 | app/src/main/python/speech.py:1 | 166 | f5ba01373d1944e7a94d9550c96f26c0011c4962b7abcf8369f1f9fb0a5b0491 |
| 正式 | app/src/main/python/photo_import.py:1 | 192 | 3f462ad8036d8e9e7c526c544555def922fe729d804eb46ee41cabf0f7c32913 |
| 正式 | app/src/main/python/drawthings.py:1 | 87 | cfff78c103391a6896cd0afd4c105ed7753f0fad6e2f5613f0a96dfec408b0ce |
| 正式 | app/src/main/python/scripts/compose_video.py:1 | 351 | cb1286191e725a881c39bf7dec575d16bb02f0e38d66fd94408831da80156785 |
| 正式 | app/src/main/python/scripts/make_homepage_bg.py:1 | 365 | 8913b24dddc3441c2eb9f54ba959a083ab081530da4d798a9162149b7f19d75c |
| 正式 | app/src/main/java/cn/tingwen/workbench/ExportProvider.java:1 | 60 | f93953234a1a139a7452b3eb01114215d6a748b6310d2ec8b1076c646d81bfe5 |
| 正式 | app/src/main/java/cn/tingwen/workbench/StartupTiming.java:1 | 14 | cdf5905c9736b04eba66e182ec6f06e83ab90b856adbf3dcc703395184be6097 |
| 正式 | app/src/main/java/cn/tingwen/workbench/MainActivity.java:1 | 389 | 4a5a4285552ff0bb87f0fecdb314e77825e571d9e6104e20a80228c921050304 |
| 正式 | app/src/main/java/cn/tingwen/workbench/ExportPolicy.java:1 | 50 | 217beee04c9a44ae11c1f1b7f452b1c2c7d9b4476b3e5aa7d7342278bf404e15 |
| 正式 | app/src/main/java/cn/tingwen/workbench/WorkbenchService.java:1 | 105 | 7480ffe1e2d413f7b3beee91e50aff31f7e10fc78e786518ac9b8ab5318dd86e |
| 正式 | app/src/main/java/cn/tingwen/workbench/StartupAnimationView.java:1 | 102 | d45a15aa0648ba946a2c4c5b5d7fc4d3ec404f9e7abbb5f68f89d5d85542ab84 |
| 正式 | app/src/main/assets/workbench/templates/index.html:1 | 284 | 4581eb0e945b530bf917b328b90c093c2252697213ea9bff275fc2d5f7ae3eeb |
| 正式 | app/src/main/assets/workbench/static/mobile.js:1 | 102 | 7a8f6fb62a6c96c4b1e18a85fd24c9394f5f81436672643012692efbbb229547 |
| 正式 | app/src/main/assets/workbench/static/mobile.css:1 | 19 | e1129160377f265f815c0699babfbc359c625192f9379147ddf9844ceb27a678 |
| 正式 | scripts/build-apk.sh:1 | 27 | 88626ca5ef65c76cf097f767d6703e3f6969a7257b324dc2933a0b5af7170ecc |
| 正式 | scripts/build-media.sh:1 | 86 | d2548b0ff2aa4add35bee41dc30a0ecbc94f5cf9020b3d22aefc4359613874f6 |
| 正式 | scripts/build-pillow-libs.sh:1 | 82 | 73b9c4605066cd4fd656aa13c1b7b4b682ca317169b36db82599c61b9e410ecd |
| 正式 | scripts/test-parity.py:1 | 198 | 28bb7e8ff1be4e8cbfec14b9e954f6d37606f8d273ca295ab02721f14048c3c7 |
| 试用 | app/build.gradle:1 | 80 | 43bdb0dd1e3690cd7fef42b92724346c33d055dba4aa249576779e5b36858985 |
| 试用 | build.gradle:1 | 4 | 09a91ee8bb9da6698ebcfc5a6550ba6b20821d20a62a0f7be83f6a70cc2bf2b4 |
| 试用 | app/src/main/AndroidManifest.xml:1 | 28 | d389a4a2652adca5ac0bf8bea08237cacdd9404da323e6a53a2c2ce261c82a0f |
| 试用 | app/src/main/python/trial_limits.py:1 | 271 | 3f87b1e6508a104143287384f00e05a5627f5dd2eaf749e72bbb8d6b912f41c4 |
| 试用 | app/src/main/python/app.py:1 | 1023 | acf97199f41937f1d61fad890495eb3278dee26e0f89aea567d1d41ddcdbd220 |
| 试用 | app/src/main/assets/workbench/templates/index.html:1 | 307 | 85c806e86019ea624ade9ab86190c328c9a26c04c376008495b00428f87607a8 |
| 试用 | app/src/main/assets/workbench/static/mobile.js:1 | 136 | 1d7bd99597ab28db2a6c18f8863f00ea3e3e57151526a4f71e7e0153c8fb7d5d |
| 试用 | app/src/main/java/cn/tingwen/workbench/MainActivity.java:1 | 443 | 5d6a8be6f49e3eb279b2a4ac1604fc9c825c3cb476f5f0706bb66d1dbc35d6d9 |
原始交付证据
- 正式
docs/1.0.2交付验证.json、docs/1.0.2-PC回归结果.json、docs/1.0.2更新说明.md和截图。 - 试用
docs/1.0.2交付记录.json、docs/累计试用版回归结果.json、docs/试用版APK验证.json及其研究合成接口样例。 - 两版
docs/GitHub1.0.2发布核验.json:远端附件状态、字节数、服务端SHA256与桌面一致的核验。 - 原生
native-sources、native-compat、licenses、tooling/media/provenance:精确源归档、编译配置及静态检查。 - HTML随附研究依据稿、源Markdown、生成脚本和HTML阅读核验记录;这些用于解释资料来源,不取代源码。
外部一手参考
以下用于核对平台/组件机制,不用来证明本APK已通过设备全流程。本文主体实现事实来自本项目源码和交付报告。
| 参考 | 用途 |
|---|---|
| Chaquopy17 Gradle | Python运行时/Java接入和打包机制 |
| Android SAF | 文档选择、用户URI与创建文件机制 |
| Android16KB支持 | 原生库和页大小适配背景 |
| Android前台服务类型 | specialUse等平台机制 |
| FFmpeg格式文档 | concat和MP4索引机制 |
| edge-tts上游 | 在线语音接入组件及其使用边界 |
参考访问日期2026-10-10。外部文档未来可能变化,APK固定依赖和本次代码基线以本页版本表与源哈希为准。
技术术语
| 术语 | 本项目定义 |
|---|---|
| Loopback | 手机本机127.0.0.1;不经过PC,也不对局域网监听 |
| NativeBridge | 主Activity内部受限JavaScript接口,与Python业务通过Chaquopy调用 |
| ACTIVE/Control | 运行中步骤集合/任务共享暂停取消和内存凭据对象 |
| image_version/digest | 图片集标识/当前有序图片字节摘要,用于确认一致性 |
| pending/partial | 尚未完成的写入文件,不能作为可导出成品 |
| SubmissionUnknown | 云端结果可能完成/计费但本机不确定,禁止自动重发 |
| receipt | 试用照片预选内容绑定凭据,避免同批提交重复扣额 |
| code point | Unicode码点,与视觉字形/UTF-16码元不同 |
| ABI/PIE/ELF | Android架构接口/位置无关可执行程序/原生程序文件格式 |
| SAF/content URI | 系统文档访问框架/受授权内容文件地址 |
| fsync/replace | 尽力持久同步/临时文件到正式文件的原子替换;非全局数据库事务 |
| FGS/WakeLock | 前台服务/部分CPU唤醒机制;不是后台永不终止承诺 |
维护或分发时,应同时保留版本基线、对应源码、许可证、构建步骤和实际验证范围;升级不能把旧文档的测试结论自动继承为新APK设备全流程已通过。