1. 引言
-
本节为非规范性内容。*
手写输入是绘画。绘画捕获了能够数字化重现人类笔尖运动所需的信息。
本API旨在将操作系统的能力暴露给Web。我们预计手写识别能力会因操作系统而异,因此API旨在达成能够灵活集成操作系统特性的设计。
我们期望用户代理将Web API的数据结构(本规范中定义)转换为宿主操作系统可用的结构,并将Web API与操作系统API连接起来。
本API不试图定义在所有平台上行为一致的识别。
1.1. 术语定义
在本规范中,我们定义了如下概念,以手写“WEB”为例:-
绘画(drawing)由多个笔画组成(如上图字母E包含三笔)。
-
笔画(stroke)表示一次在一段时间内连续的笔尖运动(例如从一次
touchstart事件到对应的touchend事件)。其运动轨迹由一系列点(points)描述。 -
点(point)是时空中笔尖的观测记录。其记录了笔尖在书写表面上的时间戳和位置(例如
touchmove事件)。 -
转写(transcription) 是表示绘画中书写文本的Unicode字符串(如字符串"WEB")。
手写识别器(handwriting recognizer) 是一个接口(通常由外部应用或服务实现),它:
-
将绘画(drawing)作为输入
-
输出该绘画的若干转写(transcriptions)
-
可选地,输出每个转写的切分(segmentation)信息
是否为手写识别器由用户代理自行决定。
要转换(convert)与手写识别器适配的数据格式,用户代理应将本规范中的定义与手写识别器的等价概念对应。
手写识别器还可以输出额外信息帮助Web应用更好地处理手写内容(如删除手写中的某个字符)。
切分(Segmentation)用于将字素(用户感知的字符)与其组成的笔划和点进行映射。一个字素可能跨多个Unicode码点。
以手写文本 "int" 为例:
-
第1和第5笔组成字母"i"
-
第2笔组成字母"n"
-
第3和第4笔组成字母"t"
2. API惯用法
本规范中提及的任务源是手写识别任务源(handwriting recognition task source)。
当某个算法排入一个手写识别API任务 T时,用户代理必须使用全局对象的手写识别任务源将T排入当前realm记录的任务队列。
除非特别说明,通过算法步骤构造的JavaScript对象的realm应为当前realm记录。
3. 能力查询
能力查询接口允许Web应用查询实现相关的能力,从而决定是否使用该功能。
const modelConstraint= { languages: [ 'zh-CN' , 'en' ] }; const modelDesc= await navigator. queryHandwritingRecognizer( modelConstraint); // \`modelDesc\` 描述了满足 \`modelConstraint\` 的手写识别器。 // 如果约束无法被满足,\`modelDesc\` 为null。 { textAlternatives: true , textSegmentation: true , hints: { alternatives: true , textContext: true , inputTypes: [ 'mouse' , 'touch' , 'stylus' ] } }
[SecureContext ]partial interface Navigator {Promise <HandwritingRecognizerQueryResult ?>(queryHandwritingRecognizer HandwritingModelConstraint ); };constraint dictionary {HandwritingModelConstraint required sequence <DOMString >; };languages dictionary {HandwritingRecognizerQueryResult boolean ;textAlternatives boolean ;textSegmentation HandwritingHintsQueryResult ; };hints dictionary {HandwritingHintsQueryResult sequence <HandwritingRecognitionType >;recognitionType sequence <HandwritingInputType >;inputType boolean ;textContext boolean ; };alternatives enum {HandwritingRecognitionType ,"text" };"per-character" enum {HandwritingInputType ,"mouse" ,"stylus" };"touch"
3.1.
queryHandwritingRecognizer(constraint)
该方法为Web应用提供了一种查询底层识别器能力并决定是否使用的方法:
-
如果约束可被满足,则返回满足约束的手写识别器描述,
-
如果约束无法被满足,则返回
null
同一份HandwritingModelConstraint
可用于调用createHandwritingRecognizer(constraint)
去创建满足约束的HandwritingRecognizer。
queryHandwritingRecognizer(constraint)方法时,按以下步骤执行:
-
如果constraint不包含
languages成员,则返回一个被拒绝的Promise,异常类型为TypeError。 -
令p为一个新的Promise。
-
按如下步骤并行运行:
-
若满足下列任何一项:
排入手写识别API任务,以
null解析p并终止剩余操作。 -
否则,排入手写识别API任务:
-
令result为一个新的
HandwritingRecognizerQueryResult -
转换识别器特性描述,并填充result的所有成员。
-
解析p为result。
-
-
返回p。
将数据转换为HandwritingRecognizerQueryResult
和HandwritingHintsQueryResult时实现应遵循:
-
如果识别器不接受任何hint,
hints设为null。 -
不支持的特性或hint,其属性设为
null。 -
若支持某个枚举hint,属性值为可选值的列表。
-
若支持某个非枚举hint,属性值为
true。
3.2.
HandwritingModelConstraint
属性
描述底层手写识别器(如后续要创建)的约束条件。
在createHandwritingRecognizer(constraint)中也用于创建手写识别器。
languages- 用于描述识别器要识别的语言的一组[BCP47]语言标签。
如提供多个语言,识别器需支持全部才能满足约束。
用户代理应考虑每个语言标签所有可能的书写体系。例如,只能识别拉丁字母阿塞拜疆语("az-Latn")的识别器,不应用于"az"标签,因为阿塞拜疆语也可用西里尔字母("az-Cyrl")书写。
区分书写体系时,建议使用最具体的语言标签。例如,仅需支持拉丁字母阿塞拜疆语时用"az-Latn"。
部分识别器仅支持单语种。为提升互操作性,可为每种语言创建独立识别器。
3.3. HandwritingRecognizerQueryResult
属性
描述某手写识别器实现的固有特性。
textAlternatives- 布尔型,表示实现是否返回多个备选转写候选而不是单一结果。
textSegmentation- 布尔型,表示实现是否为每个转写返回分段信息。
hintsHandwritingHintsQueryResult对象,描述在startDrawing()中可接受的提示。
3.3.1. HandwritingHintsQueryResult
属性
描述一组可选传入startDrawing()以提升准确率或性能的提示。
本处属性名称与startDrawing()方法中一致。
recognitionType- 一组
HandwritingRecognitionType枚举,描述可能书写的文本类型。 "text"- 普通书写语句中的自由文本,意味着图像为真实词语。例如用于日常语言句子的手写输入。
"per-character"- 由独立且互无关联的字素组成的手写体(用户感知字符)。如序列号、注册码等。
inputType- 一组
HandwritingInputType枚举,描述绘画是如何书写的。 "touch"- 用手指绘制。
"stylus"- 用手写笔绘制。
"mouse"- 用鼠标光标绘画。
textContext- 布尔值,表示是否接受textContext。
textContext为展示给用户的文本或前置已识别文本的字符串。 alternatives- 布尔值,表示是否允许设置返回备选转写的个数。用于限制
getPrediction返回的最大备选数量。
提示不能保证转写结果符合指定类型。
4. 创建手写识别器
A HandwritingRecognizer
管理执行识别所需的资源。
const modelConstraint= { languages: [ 'en' ] }; try { const recognizer= await navigator. createHandwritingRecognizer( modelConstraint); // Use \`recognizer\` to perform recognitions. } catch ( err) { // The provided model constraint can't be satisfied. }
[SecureContext ]partial interface Navigator {Promise <HandwritingRecognizer >(createHandwritingRecognizer HandwritingModelConstraint ); };constraint
4.1.
createHandwritingRecognizer(constraint)
方法
此方法创建一个满足所提供 HandwritingRecognizer
约束 HandwritingModelConstraint
的对象,并保留执行识别所需的资源。它代表了访问handwriting
recognizer的入口点。
-
如果约束可被满足,且操作系统上有足够资源执行识别,则解析为一个
HandwritingRecognizer对象。 -
否则,拒绝并返回错误。
用户代理可能会提示用户安装或下载手写模型。Web 应用不应假定此方法总是会很快返回。
createHandwritingRecognizer(constraint)
方法被调用时,执行以下操作:
-
如果 constraint 不包含
languages成员,则返回一个被拒绝的 Promise,异常类型为TypeError。 -
令 p 为 一个新的 Promise。
-
按如下步骤并行运行:
-
将 constraint 转换为适合 创建平台相关 handwriting recognizer 的格式。
-
排入 Handwriting Recognition API 任务以执行:
-
如果用户代理无法创建或准备可执行识别的handwriting recognizer,则根据失败原因用新的异常 reject p:
-
如果 constraint 的
languages是空的列表,则返回"NotSupportedError"DOMException。 -
如果用户代理找不到满足转换后 constraint 的平台相关 handwriting recognizer,则返回
"NotSupportedError"DOMException。 -
如果创建一个 handwriting recognizer 会导致用户代理超出其允许的活动识别器总数限制,则返回
QuotaExceededError。 -
如果 Web 应用可以重试调用此方法,则返回
"OperationError"DOMException。 -
对于其他所有失败原因,返回
"UnknownError"DOMException。
-
-
否则:
-
令 result 为新的
HandwritingRecognizer对象。 -
将 result 与前一步创建的平台相关 handwriting recognizer 关联。
-
将 result.active 标记为
true。 -
解析 p 为 result。
-
-
-
-
返回 p
5. 使用识别器
const drawingHints= { textContext: "Hello world." } const drawing= recognizer. startDrawing( textContext) // Do something with \`drawing\`. // Frees resources associated with recognizer. recognizer. finish()
5.1.
HandwritingRecognizer
对象
[Exposed =Window ,SecureContext ]interface {HandwritingRecognizer HandwritingDrawing (startDrawing optional HandwritingHints = {});hints undefined (); };finish dictionary {HandwritingHints DOMString = "text";recognitionType DOMString = "mouse";inputType DOMString ;textContext unsigned long = 3; };alternatives
HandwritingRecognizer
有一个 active 标志,为布尔值。active 标志初始为
true,
在调用 finish()
方法时变为 false。
当识别器的 active 标志为 true 时,Web 应用可以为该识别器创建新的绘画并执行识别。
用户代理可以限制网站的active handwriting recognizer总数。
5.2.
startDrawing(hints)
此方法创建一个 HandwritingDrawing
用于存储后续识别的绘画信息。
HandwritingDrawing
拥有一个 strokes,它是一个 list,包含若干 HandwritingStroke,
初始为空。
HandwritingDrawing
有一个 recognizer,用于存储创建此 HandwritingDrawing
的 HandwritingRecognizer
的引用。
startDrawing(hints)
被调用时,执行以下操作:
-
如果
this.active 标志不是true,则 抛出 一个新的DOMException对象,其name成员为"InvalidStateError"并中止。 -
创建一个新的
HandwritingDrawing作为 result,如有必要在其中存储转换后的提示。 -
将 result.recognizer 设为
this。 -
返回 result。
如果提供的 hints 包含识别器不支持的特性,用户代理应忽略相关属性。
如果未提供 hints,用户代理可自行决定应用默认值。
用户代理可能会创建并传递转换后的 hints 给等价的 handwriting recognizer
的绘画对象,而不将其存储在 HandwritingDrawing
中。
5.3.
finish()
此方法将 this 的 active 标志设为 false,释放已分配的handwriting
recognizer资源,并使未来涉及 this 的相关操作失败。
用户代理应释放与handwriting recognizer相关的资源。
在调用 finish() 之后,对由 this 创建的 HandwritingDrawing
调用后续的 getPrediction()
将会失败。
6. 构建 HandwritingDrawing
HandwritingDrawing
管理关于绘画的上下文信息,并维护构成绘画的笔划和点。它代表一个 drawing。
用户代理可以将所有笔划和点存储在内存中,然后在调用 getPrediction()
时将它们转换为适合 handwriting recognizer 的格式。在这种情况下,HandwritingDrawing
和 HandwritingStroke
表现得像一个列表。
HandwritingStroke
和 HandwritingDrawing
的方法时将它们传递给 handwriting
recognizer。在这种情况下,HandwritingDrawing
和 HandwritingStroke
表现为平台相关对象的包装器,用于handwriting recognizer。
用户代理可以跟踪绘画的更改以提升 getPrediction()
的性能。此类更改跟踪可以通过在相关方法调用时将笔划标记为“已修改”来实现。更改跟踪使得用户代理能够对支持的 handwriting recognizer
执行增量识别。
例如,考虑一个绘画包含三段文本,之前的 getPrediction()
调用的预测结果已被存储。Web 应用随后在第三段中添加了一笔。有了更改跟踪,实现将只请求 handwriting recognizer
对第三段的笔划(例如包括被修改笔划附近的笔划)进行新的预测,并将该预测与现有预测合并。
const handwritingStroke= new HandwritingStroke() // Add points to a stroke. handwritingStroke. addPoint({ x: 1 , y: 2 , t: 0 }); handwritingStroke. addPoint({ x: 7 , y: 6 , t: 33 }); // Retrieve points of this stroke. // Returns a copy of all the points, modifying the returned points // has no effect. const points= handwritingStroke. getPoints(); [ { x: 1 : , t: 2 , t: 0 }, { x: 7 , y: 6 , t: 33 } ]; // Delete all points in a stroke. handwritingStroke. clear(); // Add a stroke to the drawing. drawing. addStroke( handwritingStroke); // Get all strokes of the drawing. // Returns a list of \`HandwritingStroke\` in the drawing. Web applications can // modify the stroke. For example, calling HandwritingStroke.addPoint(). drawing. getStrokes(); [ HandwritingStroke, /* ... */ ] // Delete a stroke from the drawing. drawing. removeStroke( handwritingStroke); // Delete all strokes from the drawing. drawing. clear();
[Exposed =Window ,SecureContext ]interface {HandwritingDrawing undefined (addStroke HandwritingStroke );stroke undefined (removeStroke HandwritingStroke );stroke undefined ();clear sequence <HandwritingStroke >();getStrokes Promise <sequence <HandwritingPrediction >>(); }; [getPrediction SecureContext ,Exposed =Window ]interface {HandwritingStroke ();constructor undefined (addPoint HandwritingPoint );point sequence <HandwritingPoint >();getPoints undefined (); };clear dictionary {HandwritingPoint required double ;x required double ; // Optional. Number of milliseconds since a reference time point for a // drawing.y DOMHighResTimeStamp ; };t
HandwritingPoint
中提供了 t,
则应为给定的 HandwritingDrawing
使用共同的起点来测量所有的 t 值。
例如,可以将 t === 0 定义为调用 startDrawing()
的时间点。或者在采集点(例如在发生 touchmove 事件)时使用
Date.now()。
6.1. HandwritingStroke
HandwritingStroke
表示一个 stroke。它存储了重现该次笔尖运动所需的信息。
HandwritingStroke
有一个 Points,它是一个 list,用于存储该笔划的 points。points
初始为空。
6.1.1.
HandwritingStroke()
-
创建一个新的
HandwritingStroke对象,令其为 result。 -
返回 result。
6.1.2.
addPoint(point)
this 添加一个 point,
执行以下操作:
-
令 p 为一个新对象,
-
将 p.
x设为 point.x -
将 p.
y设为 point.y -
如果 point 包含
t成员,设 p.t为 point.t
如果 point 没有 t 成员,实施不应插值或使用默认数值。实现应反映出 point.t 未被设置在
p.t 中。
在调用 addPoint(point)
之后修改 point 不会影响 HandwritingStroke。
6.1.3.
getPoints()
该方法返回该笔划的所有点。
对 getPoints()
返回值的修改不会影响此笔划本身。
6.1.4. clear()
该方法会移除当前笔划的所有点,使其变为空笔划。
6.2.
HandwritingDrawing
6.2.1.
addStroke(stroke)
-
如果 stroke 不是
HandwritingStroke的实例, 抛出 一个新的TypeError并中止。
6.2.2.
removeStroke(stroke)
-
如果 stroke 不是
HandwritingStroke的实例, 抛出 一个新的TypeError并中止。
6.2.3.
getStrokes()
该方法返回该绘画中的笔划列表。
6.2.4.
clear()
7. 获取 HandwritingDrawing
的识别结果
// 获取该绘画笔划的识别结果。 const predictions= await drawing. getPrediction(); // \`predictions\` 是一个 \`HandwritingPrediction\` 列表,其属性 // 取决于手写识别器的能力,和 queryHandwritingRecognizer() 返回的模型描述保持一致。 // // 例如,\`recognizer\` 支持 textSegmentation。 [ { text: "hello" , segmentationResult: [ { grapheme: "h" , beginIndex: "0" , endIndex: "1" , drawingSegments: [ { strokeIndex: 1 , beginPointIndex: 0 , endPointIndex: 32 }, { strokeIndex: 2 , beginPointIndex: 0 , endPointIndex:: 40 }, ] }, { grapheme: "2" , beginIndex: "1" , endIndex: "2" , drawingSegments: [ { strokeIndex: 2 , beginPointIndex: 41 , endPointIndex:: 130 }, ] }, // ... ] }, { text: "he11o" , segmentationResult: [ /* ... */ ] }, // 最多返回 \`alternatives\` 个预测结果(如在 startDrawing() 里配置)。 ];
7.1.
getPrediction()
getPrediction()
方法返回 this 绘画的预测结果列表及其元数据。
预测结果按置信度高到低排序。如果不为空,第一个预测结果应为最有可能的结果。
如果手写识别器无法识别出任何内容,getPrediction()
应返回空列表。
用户代理可能会进行变更跟踪以及增量识别以提升性能。
getPrediction()
时:
-
如果
this.recognizer.active 不为 true,则返回一个被拒绝的 promise,其原因是"InvalidStateError"DOMException。 -
令 p 为新的 Promise,并 并行运行以下步骤:
-
将转换后的 drawing 发送给手写识别器。
-
等待手写识别器返回其预测结果。
-
排入手写识别 API 任务队列以执行以下步骤:
-
令 result 为列表。
-
对于每个返回的预测 pred:
-
将 pred 转为
HandwritingPredictionidl_pred。 -
将 idl_pred 添加到 result。
-
-
用 result 解析 p。
-
-
-
返回 p
7.2. HandwritingPrediction
属性
HandwritingPrediction
表示来自手写识别器的预测结果。
dictionary {HandwritingPrediction required DOMString ;text sequence <HandwritingSegment >; };segmentationResult dictionary {HandwritingSegment required DOMString ;grapheme required unsigned long ;beginIndex required unsigned long ;endIndex required sequence <HandwritingDrawingSegment >; };drawingSegments dictionary {HandwritingDrawingSegment required unsigned long ;strokeIndex required unsigned long ;beginPointIndex required unsigned long ; };endPointIndex
text- 表示所绘内容转录的
DOMString。 segmentationResult-
一个
HandwritingSegment列表, 将每个被识别的音位(用户感知的字符)映射到其组成的笔画与点。Web应用可以通过
textSegmentation检查该属性是否为null。
7.2.1. HandwritingSegment
属性
HandwritingSegment
描述从绘图分段出的单个音位。
grapheme- 表示音位的
DOMString。 beginIndex- 该音位在
text中的起始索引。 endIndex-
该音位在
text中的结束索引(下一个音位的起始点)。 drawingSegments-
一个
HandwritingDrawingSegment列表, 描述组成该音位的绘图部分。
用beginIndex
与endIndex
对text
进行切片,应当得到grapheme。
// Web 应用可使用 \`beginIndex\` 和 \`endIndex\` 对 \`text\` 进行切片。 // 例如,“घोषित”的 \`HandwritingPrediction\`: const prediction= { // UTF-16 code points: \u0918 \u094b \u0937 \u093f \u0924 // Graphemes: घो, षि, त text: "घोषित" , segmentationResult: [ { grapheme: "घो" , beginIndex: "0" , endIndex: "2" }, { grapheme: "षि" , beginIndex: "2" , endIndex: "4" }, { grapheme: "त" , beginIndex: "4" , endIndex: "5" }, ] } // 以下均成立: prediction. text. slice( 0 , 1 ) === "घो" ; prediction. text. slice( 2 , 4 ) === "षि" ; prediction. text. slice( 4 , 5 ) === "त" ; // Web 应用可以通过 splice 删除第2个音位(षि)。 const withoutSecondGrapheme= ( [... prediction. text] . splice( prediction. segmentationResult[ 1 ]. beginIndex, prediction. segmentationResult[ 1 ]. endIndex) . join( '' ) ); // => "घोत"
7.2.2.
HandwritingDrawingSegment
属性
HandwritingDrawingSegment
描述HandwritingStroke
的一个连续片段。
属性基于HandwritingStroke
在HandwritingDrawing
调用getPrediction()时的内容。
strokeIndexHandwritingDrawing中HandwritingStroke的索引。参见strokes。beginIndex- 该绘图片段的起始索引。
endIndex- 该绘图片段的结束索引(下一个绘图片段的起始点)。
8. 隐私注意事项
本节为非规范性内容。指纹向量来源于两部分:特性检测和识别器实现。
所暴露的信息量(熵)取决于用户代理的实现。我们认为没有一种方案可适用于所有场景,建议用户代理根据自身用户情况决定是否需要隐私保护措施(例如权限提示)。
特性检测可能会暴露以下信息:
-
用户的语言选择(或已安装的手写识别模型)。这同样可以通过
navigator.languages获取。 -
所用识别器实现,通过汇总支持的特征集合。这可能会被用来推断操作系统及其版本。
可通过以下方式缓解指纹收集:
-
隐私预算:若网站查询过于频繁,用户代理将拒绝 Promise。
-
权限提示:用户代理请求用户授权开放手写识别功能。
-
硬编码值:如果可以在构建时确定语言和特性,用户代理可对查询操作返回硬编码值。
识别器实现可能暴露操作系统、设备或用户习惯的信息。这主要依赖于所用的识别器技术。
以下是一些识别器实现类型及其相关风险:
-
无识别器支持:没有指纹风险。
-
基于云的识别器:风险极低或没有风险。网站可能通过对比预测结果和调用云服务直接获得的结果,检测所用云服务。但不会额外暴露用户或其设备的信息。
-
无状态模型(最常见):此类模型的输出仅取决于输入绘图和模型本身。
-
仅能通过浏览器更新的模型(例如全部在浏览器内实现):网站可获知浏览器版本,但这也可通过其他手段检测。
-
可在浏览器外进行更新的模型(例如通过操作系统 API 实现),根据场景,网站可获知:
-
如果模型随每次操作系统更新一起更新,则可得知操作系统版本。
-
如果模型仅随部分操作系统更新一起更新,则可得知某一范围的操作系统版本。
-
如果模型通过带外或按需方式单独更新,则可能获知特定补丁级别。
-
-
如模型利用硬件加速器(如 GPU),则结果可能暴露特定硬件的信息。
-
-
有状态/在线学习模型(最极端假设情况):这些模型可根据先前使用记录学习。例如,操作系统识别器根据用户输入法习惯调整输出。这类模型可能泄露大量用户信息,风险极高。
然而,目前我们尚未发现属于此类的识别器实现。但我们建议对这类模型采取隐私保护措施,或者为每次会话使用全新、干净的状态。
指纹收集成本:指纹收集方需精心设计并准备一系列手写绘图(对抗性样本)以利用模型差异。生成这些样本可能成本较高,但可以认为有动力的一方能够获得这些样本。