Initial commit
This commit is contained in:
commit
bc77006352
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial version.
|
39
README.md
Normal file
39
README.md
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<!--
|
||||||
|
This README describes the package. If you publish this package to pub.dev,
|
||||||
|
this README's contents appear on the landing page for your package.
|
||||||
|
|
||||||
|
For information about how to write a good package README, see the guide for
|
||||||
|
[writing package pages](https://dart.dev/guides/libraries/writing-package-pages).
|
||||||
|
|
||||||
|
For general information about developing packages, see the Dart guide for
|
||||||
|
[creating packages](https://dart.dev/guides/libraries/create-library-packages)
|
||||||
|
and the Flutter guide for
|
||||||
|
[developing packages and plugins](https://flutter.dev/developing-packages).
|
||||||
|
-->
|
||||||
|
|
||||||
|
TODO: Put a short description of the package here that helps potential users
|
||||||
|
know whether this package might be useful for them.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
TODO: List what your package can do. Maybe include images, gifs, or videos.
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
TODO: List prerequisites and provide or point to information on how to
|
||||||
|
start using the package.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
TODO: Include short and useful examples for package users. Add longer examples
|
||||||
|
to `/example` folder.
|
||||||
|
|
||||||
|
```dart
|
||||||
|
const like = 'sample';
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional information
|
||||||
|
|
||||||
|
TODO: Tell users more about the package: where to find more information, how to
|
||||||
|
contribute to the package, how to file issues, what response they can expect
|
||||||
|
from the package authors, and more.
|
30
analysis_options.yaml
Normal file
30
analysis_options.yaml
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# This file configures the static analysis results for your project (errors,
|
||||||
|
# warnings, and lints).
|
||||||
|
#
|
||||||
|
# This enables the 'recommended' set of lints from `package:lints`.
|
||||||
|
# This set helps identify many issues that may lead to problems when running
|
||||||
|
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||||
|
# style and format.
|
||||||
|
#
|
||||||
|
# If you want a smaller set of lints you can change this to specify
|
||||||
|
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||||
|
# (the recommended set includes the core lints).
|
||||||
|
# The core lints are also what is used by pub.dev for scoring packages.
|
||||||
|
|
||||||
|
include: package:lints/recommended.yaml
|
||||||
|
|
||||||
|
# Uncomment the following section to specify additional rules.
|
||||||
|
|
||||||
|
# linter:
|
||||||
|
# rules:
|
||||||
|
# - camel_case_types
|
||||||
|
|
||||||
|
# analyzer:
|
||||||
|
# exclude:
|
||||||
|
# - path/to/excluded/files/**
|
||||||
|
|
||||||
|
# For more information about the core and recommended set of lints, see
|
||||||
|
# https://dart.dev/go/core-lints
|
||||||
|
|
||||||
|
# For additional information about configuring this file, see
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
15
example/perspective_api_example.dart
Normal file
15
example/perspective_api_example.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'package:perspective_api/perspective_api.dart';
|
||||||
|
|
||||||
|
void main(List<String> args) async {
|
||||||
|
final client = PerspectiveApi(
|
||||||
|
apiKey: 'AIzaSyBbovs3G9iJBycX_K_JmfXDkoi0lRj-ifA',
|
||||||
|
);
|
||||||
|
|
||||||
|
final response = await client.analyzeComment(
|
||||||
|
'Salut, belle gosse 😏',
|
||||||
|
requestedAttributes: {RequestedAttribute.flirtation},
|
||||||
|
languages: {Language.french},
|
||||||
|
);
|
||||||
|
|
||||||
|
print(response);
|
||||||
|
}
|
7
lib/perspective_api.dart
Normal file
7
lib/perspective_api.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
library perspective_api;
|
||||||
|
|
||||||
|
export 'src/perspective_api.dart';
|
||||||
|
export 'src/models/analyze_comment_response.dart';
|
||||||
|
export 'src/models/language.dart';
|
||||||
|
export 'src/models/requested_attribute.dart';
|
||||||
|
export 'src/models/attribute_score.dart';
|
71
lib/src/http/request.dart
Normal file
71
lib/src/http/request.dart
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import 'package:http/http.dart';
|
||||||
|
|
||||||
|
/// An HTTP request to be made against the API.
|
||||||
|
abstract class HttpRequest {
|
||||||
|
/// The route for this request.
|
||||||
|
final String route;
|
||||||
|
|
||||||
|
/// The method for this request.
|
||||||
|
final String method;
|
||||||
|
|
||||||
|
/// The query parameters for this request.
|
||||||
|
final Map<String, String> queryParameters;
|
||||||
|
|
||||||
|
/// The headers for this request.
|
||||||
|
final Map<String, String> headers;
|
||||||
|
|
||||||
|
/// Create a new [HttpRequest].
|
||||||
|
///
|
||||||
|
/// {@macro http_request}
|
||||||
|
HttpRequest(
|
||||||
|
this.route, {
|
||||||
|
this.method = 'GET',
|
||||||
|
this.queryParameters = const {},
|
||||||
|
this.headers = const {},
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Transform this [HttpRequest] into a [BaseRequest] to be sent.
|
||||||
|
BaseRequest prepare();
|
||||||
|
|
||||||
|
Uri _getUri() => Uri.https(
|
||||||
|
'commentanalyzer.googleapis.com',
|
||||||
|
'/v1alpha1/$route',
|
||||||
|
queryParameters.isNotEmpty ? queryParameters : null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'HttpRequest($method $route)';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An [HttpRequest] with a JSON body.
|
||||||
|
class BasicRequest extends HttpRequest {
|
||||||
|
/// The `Content-Type` header for JSON requests.
|
||||||
|
static const jsonContentTypeHeader = {'Content-Type': 'application/json'};
|
||||||
|
|
||||||
|
/// The JSON-encoded body of this request.
|
||||||
|
///
|
||||||
|
/// Set to `null` to send no body.
|
||||||
|
final String? body;
|
||||||
|
|
||||||
|
/// Create a new [BasicRequest].
|
||||||
|
BasicRequest(
|
||||||
|
super.route, {
|
||||||
|
this.body,
|
||||||
|
super.method,
|
||||||
|
super.queryParameters,
|
||||||
|
super.headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Request prepare() {
|
||||||
|
final request = Request(method, _getUri());
|
||||||
|
request.headers.addAll(headers);
|
||||||
|
|
||||||
|
if (body != null) {
|
||||||
|
request.headers.addAll(jsonContentTypeHeader);
|
||||||
|
request.body = body!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
}
|
50
lib/src/models/analyze_comment_response.dart
Normal file
50
lib/src/models/analyze_comment_response.dart
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:perspective_api/src/models/attribute_score.dart';
|
||||||
|
import 'package:perspective_api/src/models/language.dart';
|
||||||
|
|
||||||
|
import 'requested_attribute.dart';
|
||||||
|
|
||||||
|
class AnalyzeCommentResponse {
|
||||||
|
/// The requested languages.
|
||||||
|
final Set<Language> languages;
|
||||||
|
|
||||||
|
/// The auto-detected languages.
|
||||||
|
final Set<Language> detectedLanguages;
|
||||||
|
|
||||||
|
/// The attribute scores for this comment.
|
||||||
|
final List<AttributeScore> attributeScores;
|
||||||
|
|
||||||
|
const AnalyzeCommentResponse({
|
||||||
|
required this.languages,
|
||||||
|
required this.detectedLanguages,
|
||||||
|
required this.attributeScores,
|
||||||
|
});
|
||||||
|
|
||||||
|
static AnalyzeCommentResponse parseAnalyzeCommentResponse(Map<String, Object?> res) {
|
||||||
|
final {'languages': languages as List, 'detectedLanguages': detectedLanguages as List, 'attributeScores': rawAttributeScores as Map} = res;
|
||||||
|
|
||||||
|
final attributeScores = <AttributeScore>[];
|
||||||
|
|
||||||
|
for (final ra in rawAttributeScores.keys) {
|
||||||
|
final attribute = RequestedAttribute.fromScreamingSnakeCase(ra);
|
||||||
|
|
||||||
|
final attributeScore = AttributeScore.parseAttributeScore(rawAttributeScores[ra] as Map<String, Object?>, attribute);
|
||||||
|
|
||||||
|
attributeScores.add(attributeScore);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnalyzeCommentResponse(
|
||||||
|
languages: languages.map((l) => Language.fromCode(l)).toSet(),
|
||||||
|
detectedLanguages: detectedLanguages
|
||||||
|
.map((dl) {
|
||||||
|
try {
|
||||||
|
return Language.fromCode(dl);
|
||||||
|
} on ArgumentError {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.whereType<Language>()
|
||||||
|
.toSet(),
|
||||||
|
attributeScores: attributeScores,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
72
lib/src/models/attribute_score.dart
Normal file
72
lib/src/models/attribute_score.dart
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import 'package:perspective_api/src/models/requested_attribute.dart';
|
||||||
|
|
||||||
|
class AttributeScore {
|
||||||
|
/// The requested attribute.
|
||||||
|
final RequestedAttribute attribute;
|
||||||
|
|
||||||
|
/// The summary score for this attribute.
|
||||||
|
final double summaryScore;
|
||||||
|
|
||||||
|
/// The span scores for this attribute.
|
||||||
|
final List<SpanScore> spanScores;
|
||||||
|
|
||||||
|
const AttributeScore({
|
||||||
|
required this.attribute,
|
||||||
|
required this.summaryScore,
|
||||||
|
required this.spanScores,
|
||||||
|
});
|
||||||
|
|
||||||
|
static AttributeScore parseAttributeScore(Map<String, Object?> res, RequestedAttribute attribute) {
|
||||||
|
final {
|
||||||
|
'summaryScore': sum as Map,
|
||||||
|
'spanScores': spanScores as List,
|
||||||
|
} = res;
|
||||||
|
|
||||||
|
final summaryScore = sum['value'] as double;
|
||||||
|
|
||||||
|
return AttributeScore(
|
||||||
|
attribute: attribute,
|
||||||
|
summaryScore: summaryScore,
|
||||||
|
spanScores: spanScores.map((ss) => SpanScore.parseSpanScore(ss)).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SpanScore {
|
||||||
|
/// The beginning of the span.
|
||||||
|
final int begin;
|
||||||
|
|
||||||
|
/// The end of the span.
|
||||||
|
final int end;
|
||||||
|
|
||||||
|
/// The score for this span.
|
||||||
|
final double score;
|
||||||
|
|
||||||
|
// /// The type of the span.
|
||||||
|
// final String type;
|
||||||
|
|
||||||
|
const SpanScore({
|
||||||
|
required this.begin,
|
||||||
|
required this.end,
|
||||||
|
required this.score,
|
||||||
|
// required this.type,
|
||||||
|
});
|
||||||
|
|
||||||
|
static SpanScore parseSpanScore(Map<String, Object?> res) {
|
||||||
|
final {
|
||||||
|
'begin': begin as int,
|
||||||
|
'end': end as int,
|
||||||
|
'score': sc as Map,
|
||||||
|
// 'type': type as String,
|
||||||
|
} = res;
|
||||||
|
|
||||||
|
final score = sc['value'] as double;
|
||||||
|
|
||||||
|
return SpanScore(
|
||||||
|
begin: begin,
|
||||||
|
end: end,
|
||||||
|
score: score,
|
||||||
|
// type: type,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
33
lib/src/models/language.dart
Normal file
33
lib/src/models/language.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
enum Language {
|
||||||
|
english('en'),
|
||||||
|
arabic('ar'),
|
||||||
|
chinese('zh'),
|
||||||
|
czech('cs'),
|
||||||
|
dutch('nl'),
|
||||||
|
french('fr'),
|
||||||
|
german('de'),
|
||||||
|
italian('it'),
|
||||||
|
hindi('hi'),
|
||||||
|
hinglish('hi-Latn'),
|
||||||
|
indonesian('id'),
|
||||||
|
japanese('ja'),
|
||||||
|
korean('ko'),
|
||||||
|
polish('pl'),
|
||||||
|
portuguese('pt'),
|
||||||
|
russian('ru'),
|
||||||
|
spanish('es'),
|
||||||
|
swedish('sv');
|
||||||
|
|
||||||
|
final String code;
|
||||||
|
|
||||||
|
const Language(this.code);
|
||||||
|
|
||||||
|
static Language fromCode(String code) => Language.values.firstWhere(
|
||||||
|
(e) => e.code == code,
|
||||||
|
orElse: () => throw ArgumentError.value(
|
||||||
|
code,
|
||||||
|
'code',
|
||||||
|
'Invalid language code.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
88
lib/src/models/requested_attribute.dart
Normal file
88
lib/src/models/requested_attribute.dart
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
|
enum RequestedAttribute {
|
||||||
|
toxicity,
|
||||||
|
severeToxicity,
|
||||||
|
identityAttack,
|
||||||
|
insult,
|
||||||
|
profanity,
|
||||||
|
threat,
|
||||||
|
|
||||||
|
// Experimental attributes.
|
||||||
|
@experimental
|
||||||
|
toxicityExperimental,
|
||||||
|
@experimental
|
||||||
|
severeToxicityExperimental,
|
||||||
|
@experimental
|
||||||
|
identityAttackExperimental,
|
||||||
|
@experimental
|
||||||
|
insultExperimental,
|
||||||
|
@experimental
|
||||||
|
profanityExperimental,
|
||||||
|
@experimental
|
||||||
|
threatExperimental,
|
||||||
|
@experimental
|
||||||
|
sexuallyExplicit,
|
||||||
|
@experimental
|
||||||
|
flirtation,
|
||||||
|
|
||||||
|
// N-Y-T attributes.
|
||||||
|
attackOnAuthor,
|
||||||
|
attackOnCommenter,
|
||||||
|
incoherent,
|
||||||
|
inflammatory,
|
||||||
|
likelyToReject,
|
||||||
|
obscene,
|
||||||
|
spam,
|
||||||
|
unsubstantial;
|
||||||
|
|
||||||
|
String toScreamingSnakeCase() {
|
||||||
|
final split = name.split('');
|
||||||
|
StringBuffer res = StringBuffer();
|
||||||
|
|
||||||
|
for (final char in split) {
|
||||||
|
if (char != char.toLowerCase() && res.isNotEmpty) {
|
||||||
|
res.write('_');
|
||||||
|
res.write(char);
|
||||||
|
} else {
|
||||||
|
res.write(char);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.toString().toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
static RequestedAttribute fromScreamingSnakeCase(String snakeCase) {
|
||||||
|
final split = snakeCase.split('_');
|
||||||
|
StringBuffer res = StringBuffer();
|
||||||
|
|
||||||
|
for (final (i, word) in split.map((e) => e.toLowerCase()).indexed) {
|
||||||
|
if (i == 0) {
|
||||||
|
res.write(word);
|
||||||
|
} else {
|
||||||
|
res.write(word[0].toUpperCase());
|
||||||
|
res.write(word.substring(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RequestedAttribute.values.firstWhere(
|
||||||
|
(e) => e.name == res.toString(),
|
||||||
|
orElse: () => throw ArgumentError.value(
|
||||||
|
snakeCase,
|
||||||
|
'snakeCase',
|
||||||
|
'Invalid attribute name.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Set<RequestedAttribute> restrictedLanguages = {
|
||||||
|
attackOnAuthor,
|
||||||
|
attackOnCommenter,
|
||||||
|
incoherent,
|
||||||
|
inflammatory,
|
||||||
|
likelyToReject,
|
||||||
|
obscene,
|
||||||
|
spam,
|
||||||
|
unsubstantial,
|
||||||
|
};
|
||||||
|
}
|
64
lib/src/perspective_api.dart
Normal file
64
lib/src/perspective_api.dart
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:http/retry.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:perspective_api/src/http/request.dart';
|
||||||
|
|
||||||
|
import 'models/analyze_comment_response.dart';
|
||||||
|
import 'models/language.dart';
|
||||||
|
import 'models/requested_attribute.dart';
|
||||||
|
|
||||||
|
DateTime? _lastRequestTime;
|
||||||
|
|
||||||
|
final class PerspectiveApi {
|
||||||
|
final String _apiKey;
|
||||||
|
|
||||||
|
final _client = RetryClient(Client(), when: (res) {
|
||||||
|
return _lastRequestTime == null ||
|
||||||
|
_lastRequestTime!.isAfter(
|
||||||
|
DateTime.now().subtract(
|
||||||
|
const Duration(seconds: 1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
PerspectiveApi({
|
||||||
|
/// The API key to use for requests.
|
||||||
|
required String apiKey,
|
||||||
|
}) : _apiKey = apiKey;
|
||||||
|
|
||||||
|
/// Analyze a comment.
|
||||||
|
Future<AnalyzeCommentResponse> analyzeComment(
|
||||||
|
String text, {
|
||||||
|
required Set<RequestedAttribute> requestedAttributes,
|
||||||
|
required Set<Language> languages,
|
||||||
|
}) async {
|
||||||
|
final body = {
|
||||||
|
'comment': {'text': text},
|
||||||
|
'requestedAttributes': {
|
||||||
|
for (final ra in requestedAttributes)
|
||||||
|
ra.toScreamingSnakeCase(): {},
|
||||||
|
},
|
||||||
|
'languages': languages.map((l) => l.code).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final request = BasicRequest(
|
||||||
|
'comments:analyze',
|
||||||
|
queryParameters: {
|
||||||
|
'key': _apiKey,
|
||||||
|
},
|
||||||
|
body: json.encode(body),
|
||||||
|
method: 'POST',
|
||||||
|
);
|
||||||
|
|
||||||
|
final res = await _client.send(request.prepare());
|
||||||
|
|
||||||
|
_lastRequestTime = DateTime.now();
|
||||||
|
|
||||||
|
final resBody = (await res.stream.bytesToString());
|
||||||
|
|
||||||
|
final jsonBody = json.decode(resBody);
|
||||||
|
|
||||||
|
return AnalyzeCommentResponse.parseAnalyzeCommentResponse(jsonBody);
|
||||||
|
}
|
||||||
|
}
|
17
pubspec.yaml
Normal file
17
pubspec.yaml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: perspective_api
|
||||||
|
description: A starting point for Dart libraries or applications.
|
||||||
|
version: 1.0.0
|
||||||
|
# repository: https://github.com/my_org/my_repo
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ^3.0.5
|
||||||
|
|
||||||
|
# Add regular dependencies here.
|
||||||
|
dependencies:
|
||||||
|
http: ^1.1.0
|
||||||
|
meta: ^1.10.0
|
||||||
|
# path: ^1.8.0
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
lints: ^2.0.0
|
||||||
|
test: ^1.21.0
|
0
test/perspective_api_test.dart
Normal file
0
test/perspective_api_test.dart
Normal file
Loading…
x
Reference in New Issue
Block a user