Initial commit

This commit is contained in:
Rapougnac 2023-09-27 13:47:03 +02:00
commit bc77006352
No known key found for this signature in database
GPG Key ID: CF34A08277EA36AE
14 changed files with 489 additions and 0 deletions

3
CHANGELOG.md Normal file
View File

@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

39
README.md Normal file
View 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.

0
a.html Normal file
View File

30
analysis_options.yaml Normal file
View 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

View 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
View 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
View 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;
}
}

View 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,
);
}
}

View 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,
);
}
}

View 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.',
),
);
}

View 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,
};
}

View 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
View 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

View File